Cайт веб-разработчика, программиста Ruby on Rails ESV Corp. Екатеринбург, Москва, Санкт-Петербург, Новосибирск, Первоуральск

Взаимодействие Ruby и Rust с использованием Magnus: практическое руководство

Изначально моё внимание привлекло то, что для сборки Ruby (использую rbenv) с YJIT потребовался Rust. Да, компоненты Ruby уже используют код на Rust, это очевидно. Ещё очевидно, что скорость выполнения программ Ruby с использованием YJIT сокращается в 5-7 (!) раз (Ruby 4.0.6, август 2026) - это очень впечатляющий результат.

Я заинтересовался Rust, изучил его теоретически - очень хороший язык системного программирования, логичный, понятный и строгий, со своей красотой. Синтаксис Ruby и Rust даже чем-то схожи. Программы на Rust компилируются в машинный код, который, естественно, выполняется намного быстрее программ на Ruby.

Очень радует, что разработчики предусмотрели возможность удобной связки языков Ruby и Rust - в Rust есть превосходный крейт (crate) magnus ( magnus API Docs , GitHub ), который предоставляет очень удобный механизм взаимодействия программ Ruby и Rust. Дословно: High level Ruby bindings for Rust. Write Ruby extension gems in Rust, or call Ruby code from a Rust binary.

В Ruby bundler есть команда bundler gem <имя> --ext=rust (bundle gem), которая генерирует gem Ruby (библиотеку для использования в программах Ruby) сразу же с шаблоном вызова Rust из Ruby, используя крейт magnus.

Сгенерируем такой gem, назовём его gem_ruby_rust:

$ bundler gem gem_ruby_rust --ext=rust

В результате выполнения команды (пропускаем все вопросы, просто нажимаем Enter, изменить настройки bundle можно будет потом с помощью bundle config <setting_name>) у нас создаётся каталог, в котором в виде структуры каталогов и файлов располагаются компоненты gem-а:

gem_ruby_rust
|
+--- ext  # здесь располагается код Rust
|    |
|    +--- gem_ruby_rust
|         |
|         +--- src
|         |    |
|         |    +--- lib.rs # код библиотеки Rust
|         |
|         +--- Cargo.toml  # конфигурация библиотеки Rust
|
+--- lib  # здесь располагается код Ruby, определение module GemRubyRust
|    |
|    +--- gem_ruby_rust    # здесь располагается скомпилированный gem_ruby_rust.so
|
+--- Cargo.toml # файл описания проекта Rust, крейты, зависимости
+--- Gemfile    # файл описания проекта Ruby, gem-а

Я перечислил только основные компоненты, не стал перечислять все файлы и каталоги. Для понимания назначения этих файлов необходимо хотя бы базовое понимание программирования на Ruby и Rust. В принципе, это уже полноценный, работающий gem Ruby. Файл библиотеки Rust gem_ruby_rust/ext/gem_ruby_rust/src/lib.rs выглядит следующим образом:

use magnus::{function, prelude::*, Error, Ruby};

pub fn hello(subject: String) -> String {
    format!("Hello {subject}, from Rust!")
}

#[magnus::init]
fn init(ruby: &Ruby) -> Result<(), Error> {
    let module = ruby.define_module("GemRubyRust")?;
    module.define_singleton_method("hello", function!(hello, 1))?;
    Ok(())
}

#[cfg(test)]
mod tests {
    use rb_sys_test_helpers::ruby_test;
    use super::hello;

    #[ruby_test]
    fn test_hello() {
        assert_eq!("Hello world, from Rust!", hello("world".to_string()));
    }
}

Если хотите сразу проверить работоспособность gem, то внутри корневого каталога gem_ruby_rust выполняете команду (описана в gem_ruby_rust/README.mdbundle exec rake install - библиотека Rust скомпилируется, gem установится автоматически в ваш Ruby. Всё очень просто! Для проверки, можем создать мини-проект Ruby в отдельном каталоге:

# Gemfile
# здесь указываем, что используем gem GemRubyRust
# далее:
#   bundle install
#   bundle update

# для экспериментов добавим возможность измерения скорости выполнения
gem 'benchmark'

# наш gem:
gem 'gem_ruby_rust'

И сам файл тестовой программы Ruby:

# test.rb
# тестирование взаимодействия Ruby и Rust

# наш gem на Rust
require 'gem_ruby_rust'

# выполняем функцию Rust
# модуль GemRubyRust определён в библиотеке Rust
# метод hello экспортируется вызовом
# module.define_singleton_method("hello", function!(hello, 1))?;
p GemRubyRust::hello('Ruby')

Запускаем bundle install, и сразу можем проверить выполнение:

$ bundle install
$ ruby test.rb
Hello Ruby, from Rust!

$ ruby --yjit test.rb
Hello Ruby, from Rust!

Без каких либо значительных усилий и сложных программных конструкций мы получили рабочую библиотеку, написанную на Rust для Ruby. Казалось бы, ничего сложного и полезного ещё не делает эта библиотека, но в данном коде мы уже видим передачу строки как аргумента. Код Ruby очень естественно, в стиле Ruby выполняет вызов кода Rust. Как по мне, так это очень удивительно и очень удобно! К слову сказать, с использованием FFI всё это выглядело бы гораздо более громозко, неуклюже, менее понятно и менее надёжно.

Если вдруг кого-то смутит размер скомпилированной бибилиотеки gem_ruby_rust.so (а это почти 7 мегабайт), то рекомендую исправить секцию [profile.release] файла gem_ruby_rust/Cargo.toml следующим образом:

[profile.release]
debug = false
strip = true
lto = true
opt-level = "z"

Рекомендации по уменьшению размера исполняемого файла (или библиотеки) Rust хорошо описаны здесь. Размер библиотеки уменшается до примерно 300 килобайт, что по нынешним меркам не очень и много.

Тест Фибоначчи

Так уж повелось, что для оценки производительности я использую совсем простейший тест - вычисление числа Фибоначчи алгоритмом с рекурсией. Прекрасно понимаю, что есть множество специальных синтетических тестов оценки производительности выполнения программ, но, собственно, сейчас речь вообще не об этом.

Добавим свой метод вычисления числа Фибоначчи в нашу библиотеку Rust. Я привожу код полностью, далее будем предполагать, что вы понимаете, где и в каком месте расположить функции Rust и где добавить их в определение модуля, который экспортируется в Ruby.

use magnus::{function, prelude::*, Error, Ruby};

pub fn hello(subject: String) -> String {
    format!("Hello {subject}, from Rust!")
}

// +++
// добавим функцию вычисления числа Фибоначчи
// для наглядности назвал fib_rust, а экспортируется она
// в модуль Ruby как fib
pub fn fib_rust(num: i64) -> i64 {
    if num <= 1 { return num; }
    fib_rust(num - 1) + fib_rust(num - 2)
}

#[magnus::init]
fn init(ruby: &Ruby) -> Result<(), Error> {

    // модуль gema GemRubyRust
    // по этому имени он доступен в Ruby
    let module = ruby.define_module("GemRubyRust")?;

    // стандартно сгенерированный метод модуля
    module.define_singleton_method("hello", function!(hello, 1))?;

    // +++
    // добавим нашу функцию fib_rust в модуль GemRubyRust под именем метода fib
    // - первый аргумент - наименование метода в модуле GemRubyRust
    //   возможно указание и одинакового имени, например, fib и fib
    // - второй агрумент - это макрос, который указывает на нашу функцию
    //   fib_rust и указывает количество аргументов, которое принимает
    //   функция
    module.define_singleton_method("fib", function!(fib_rust, 1))?;

    Ok(())
}

#[cfg(test)]
mod tests {
    use rb_sys_test_helpers::ruby_test;
    use super::{hello, fib_rust};

    #[ruby_test]
    fn test_hello() {
        assert_eq!("Hello world, from Rust!", hello("world".to_string()));
    }

    // +++
    // так же можем добавить сразу и тестирование 
    #[ruby_test]
    fn test_fib_rust() {
        assert_eq!(0, fib_rust(0));
        assert_eq!(1, fib_rust(1));
        assert_eq!(1, fib_rust(2));
        assert_eq!(2, fib_rust(3));
    }
}

После внесённых изменений каждый раз не забываем выполнять команду bundle exec rake install для того, чтобы наша библиотека Rust скомпилировалась. Кстати говоря, это достаточно не быстрый процесс, и начинаешь понимать всю прелесть интерпретируемых языков программирования ;-) .

Наша программа Ruby выглядит следующим образом:

# test.rb
# тестирование взаимодействия Ruby и Rust

# тесты скорости
require 'benchmark'

# наш gem на Rust
require 'gem_ruby_rust'

# добавим реализацию вычисления Фибоначчи на Ruby
def fib(num)
  return num if num <=1
  fib(num - 1) + fib(num - 2)
end

Benchmark.bm do | x |

  i = 0
  5.times do

    x.report('Ruby = ') { puts fib(30+i) }
    x.report('Rust = ') { puts GemRubyRust::fib(30+i) }

    i += 1

  end

end

Запустим нашу программу.

$ ruby test.rb

832040
832040
1346269
1346269
2178309
2178309
3524578
3524578
5702887
5702887
             user     system      total        real
Ruby =   0.104618   0.000306   0.104924 (  0.104957)
Rust =   0.003563   0.000000   0.003563 (  0.003567)
Ruby =   0.169417   0.000092   0.169509 (  0.169631)
Rust =   0.005870   0.000000   0.005870 (  0.005874)
Ruby =   0.274556   0.000000   0.274556 (  0.274559)
Rust =   0.009282   0.000000   0.009282 (  0.009288)
Ruby =   0.444761   0.000000   0.444761 (  0.444783)
Rust =   0.015219   0.000000   0.015219 (  0.015232)
Ruby =   0.716754   0.000000   0.716754 (  0.716785)
Rust =   0.024088   0.000000   0.024088 (  0.024103)

# и для сравнения Ruby c YJIT
$ ruby --yjit test.rb

832040
832040
1346269
1346269
2178309
2178309
3524578
3524578
5702887
5702887
             user     system      total        real
Ruby =   0.020545   0.000170   0.020715 (  0.020999)
Rust =   0.003596   0.000000   0.003596 (  0.003758)
Ruby =   0.032330   0.000000   0.032330 (  0.032410)
Rust =   0.005710   0.000000   0.005710 (  0.005739)
Ruby =   0.052078   0.000000   0.052078 (  0.052242)
Rust =   0.009236   0.000000   0.009236 (  0.009265)
Ruby =   0.084759   0.000000   0.084759 (  0.084961)
Rust =   0.015035   0.000000   0.015035 (  0.015116)
Ruby =   0.133103   0.003982   0.137085 (  0.137494)
Rust =   0.024452   0.000000   0.024452 (  0.024523)

Как можно видеть, выйгрыш в скорости выполенения кода Rust колоссальный, а это всего лишь простенький тест, на реальных задачах выйгрыш в скорости будет ещё более значительным. Но о чём нам это говорит в данном случае? О том, что в Ruby сейчас доступны такие скорости, доступен уровень системного программирования — всё с помощью простого и лёгкого подключения кода Rust в Ruby. Так же можно заметить, что и использование YJIT в Ruby даёт очень ощутимое ускорение исполнения программ.

Передача массива Ruby в качестве аргумента в функцию Rust

Давайте немного усложним задачу — передадим в Rust массив из Ruby. Для простоты передаём массив чисел, функция Rust обрабатывает массив следующим образом — просто умножает каждый элемент на значение индекса итератора — у нас не реальные задачи, а демонстрация возможностей взаимодействия Ruby и Rust. Попутно, покажу 2 варианта: 1. создание массива внутри Rust в контексте Ruby и 2. модификация элементов самого массива, переданного из Rust.

Код нашей библиотеки Rust:

// обратить внимание:
// список используемых возможностей пополнился
use magnus::{function, prelude::*, Error, Ruby, RArray, Value, TryConvert};

pub fn hello(subject: String) -> String {
    format!("Hello {subject}, from Rust!")
}

// Фибоначчи
pub fn fib(num: i64) -> i64 {
    if num <= 1 { return num; }
    fib(num - 1) + fib(num - 2)
}

// modify_array
// результат возвращается в виде нового массива, созданного
// в Rust, но в контексте исполнения Ruby
pub fn modify_array(arr: RArray) -> Result<RArray, Error> {

    // получаем текущий Ruby handle
    let ruby = Ruby::get().unwrap();
    // массив в контексте Ruby для результата
    let result = ruby.ary_new();

    let mut i: i64 = 0;

    // пробежимся по элементам массива
    for elem in arr.into_iter() {
        let val: i64 = i64::try_convert(elem)?;
        // результат добавляем в наш созданный массив
        result.push(val * i)?;
        i += 1;
    }

    Ok(result)
}

// modify_array!
// вариант 2: модифицирует существующий массив (как Array#map!)
pub fn modify_array_inplace(arr: RArray) -> Result<RArray, Error> {

    for (i, elem) in arr.into_iter().enumerate() {
        let val: i64 = i64::try_convert(elem)?;
        arr.store(i as isize, val * (i as i64))?;
    }

    Ok(arr)
}


#[magnus::init]
fn init(ruby: &Ruby) -> Result<(), Error> {

    // модуль gema GemRubyRust
    let module = ruby.define_module("GemRubyRust")?;

    // стандартно сгенерированный метод модуля
    // метод GemRubyRust::hello
    module.define_singleton_method("hello", function!(hello, 1))?;

    // метод GemRubyRust::fib
    module.define_singleton_method("fib", function!(fib, 1))?;

    // метод GemRubyRust::modify_array
    module.define_singleton_method("modify_array", function!(modify_array, 1))?;

    // метод GemRubyRust::modify_array!
    module.define_singleton_method("modify_array!", function!(modify_array_inplace, 1))?;

    Ok(())
}

// #[cfg(test)]
// блок тестов для краткости изложения пропускаем

Использование в Ruby:

# test.rb
# тестирование взаимодействия Ruby и Rust

# наш gem на Rust
require 'gem_ruby_rust'

a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# вернёт результат в виде копии
b = GemRubyRust::modify_array(a)
p a # => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
p b # => [0, 2, 6, 12, 20, 30, 42, 56, 72, 90]

# изменяем элементы самого массива
b = GemRubyRust::modify_array!(a)
p a # => [0, 2, 6, 12, 20, 30, 42, 56, 72, 90]
p b # => [0, 2, 6, 12, 20, 30, 42, 56, 72, 90]

# сейчас a и b указывают на один и тот же объект массива, что можно проверить
# но это уже тонкости понимания механизмов Ruby, хотя в чём-то очень похоже на Rust
b[1] = 100
p a # => [0, 100, 6, 12, 20, 30, 42, 56, 72, 90]

Работа со смешанными типами данных

Вполне логично может возникнуть вопрос — если в Ruby в массиве возможно хранение элементов разных типов, но в Rust такое невозможно, то как быть в таких ситуациях? Могу сказать, что в magnus реализовано множество механизмов и возможностей — этот крейт предоставляет поистине огромные возможности взаимодействия Ruby и Rust! Допустим, наш массив Ruby будет содержать элементы: строки (String), числа с плавающей запятой (Float) и целые числа (Integer), и все остальные возможные типы, которые мы не принимаем в расчёт, а функция Rust в качестве результата нам вернёт хэш с ключами "strings", "floats" и "integers", "others", значение каждого ключа, соответственно, массивы найденных элементов в исходном массиве:

// обработка элементов смешанных типов в массиве
pub fn categorize_array(arr: RArray) -> Result<Value, Error> {

    // получаем текущий Ruby handle
    let ruby = Ruby::get().unwrap();

    // результат вернём в виде хэша
    let result = ruby.hash_new();

    // каждый массив хранит элементы своего типа
    let integers = ruby.ary_new();
    let floats   = ruby.ary_new();
    let strings  = ruby.ary_new();
    let others   = ruby.ary_new();

    // получаем классы
    // используем обращение к Ruby, чтобы получить объекты классов
    let integer_class: magnus::RClass = ruby.eval("Integer")?;
    let float_class: magnus::RClass   = ruby.eval("Float")?;
    let string_class: magnus::RClass  = ruby.eval("String")?;

    // каждый элемент исходного массива попадает в свой массив по типу
    for elem in arr.into_iter() {
        match () {
            _ if elem.is_kind_of(integer_class) => integers.push(elem)?,
            _ if elem.is_kind_of(float_class)   => floats.push(elem)?,
            _ if elem.is_kind_of(string_class)  => strings.push(elem)?,
            _ => others.push(elem)?,
        }
    }

    // результирующий хэш
    result.aset("integers", integers)?;
    result.aset("floats",   floats)?;
    result.aset("strings",  strings)?;
    result.aset("others",   others)?;

    Ok(result.as_value())
}

// далее, естественно, в init:
// module.define_singleton_method("categorize_array", function!(categorize_array, 1))?;

В Ruby получаем результат:

arr_different_types = [ 1, 2, '1', '2', 1.2, 2.3, 'str', 7.1, 9, '123', :sym, true, false ]

p GemRubyRust::categorize_array(arr_different_types)
# выводит:
# {"integers" => [1, 2, 9], "floats" => [1.2, 2.3, 7.1], "strings" => ["1", "2", "str", "123"], "others" => [:sym, true, false]}

Совсем между делом продемонстрирована возможность обращения к интерпретатору Ruby через ruby.eval ;-) и ещё внутри Rust мы сконструировали хэш и вернули его в качестве результата в Ruby. Надеюсь, сейчас уже становится понятно, насколько хорошо реализовано преобразование и работа с различными типами между Ruby и Rust — это не просто вызовы функций одного языка из другого, а полноценная корректная работа с различными типами данных, полноценное взаимодействие на уровне двух языков, что больше похоже на симбиоз, нежели на просто вызов функции и получение результата в какой-то ограниченной форме.

Вызов методов Ruby из Rust

Ну и в завершении рассмотрю ещё вызовы методов Ruby из Rust, а как же без этого — magnus позволяет построить взаимодействие в обе стороны. Например, у нас есть код Ruby:

module A
  def self.a
    puts "call 'a' from module Ruby"
    123
  end
end

class Aclass

  def a
    puts "call 'a' from obj Ruby"
  end

  def multiply(x, y)
    puts "multiply from Ruby: #{x} * #{y}"
    x * y
  end

end

a = Aclass::new

p A::a
# => call 'a' from module Ruby
#    123

p a.a
# => call 'a' from obj Ruby
#    nil

Тут всё просто и понятно. В Rust определим следующие функции, заодно обращаю внимание на функцию, которая принимает уже более одного аргумента:

// вызов метода модуля (например, A::a)
// модуль передаётся из Ruby как аргумент
pub fn call_module_method(module: Value) -> Result<i64, Error> {
    // вызываем метод "a" модуля без аргументов
    let result: i64 = module.funcall("a", ())?;
    // умножаем на 2 и возвращаем в Ruby
    Ok(result * 2)
}

// вызов метода объекта без возврата значения (возвращает nil)
pub fn call_obj_method_nil(obj: Value) -> Result<Value, Error> {
    let result: Value = obj.funcall("a", ())?;
    Ok(result)
}

// вызов метода объекта с аргументами (например, obj.multiply)
pub fn call_obj_multiply(obj: Value, x: i64, y: i64) -> Result<i64, Error> {
    // передаём аргументы как кортеж
    let result: i64 = obj.funcall("multiply", (x, y))?;
    Ok(result)
}

// не забываем добавить в init:
// инициализация
#[magnus::init]
fn init(ruby: &Ruby) -> Result<(), Error> {
    let module = ruby.define_module("GemRubyRust")?;

    module.define_singleton_method("call_module_method",  function!(call_module_method, 1))?;
    module.define_singleton_method("call_obj_method_nil", function!(call_obj_method_nil, 1))?;
    module.define_singleton_method("call_obj_multiply",   function!(call_obj_multiply, 3))?;

    Ok(())
}

И далее организуем петлю вызовов, ну просто для демонстрации: Ruby вызывает Rust, а Rust в свою очередь вызывает методы в коде Ruby:

module A
  def self.a
    puts "call 'a' from module Ruby"
    123
  end
end

class Aclass

  def a
    puts "call 'a' from obj Ruby"
  end

  def multiply(x, y)
    puts "multiply from Ruby: #{x} * #{y}"
    x * y
  end

end

a = Aclass::new

p A::a
# => call 'a' from module Ruby
#    123

p a.a
# => call 'a' from obj Ruby
#    nil

puts "call Ruby methods from Rust:"

# 1. вызов метода модуля Ruby из Rust
puts "вызов метода модуля:"
result = GemRubyRust.call_module_method(A)
puts "результат: #{result}"  # => 246

# 2. вызов метода объекта, который возвращает nil
puts "вызов метода объекта (возврат nil):"
result_nil = GemRubyRust.call_obj_method_nil(obj)
puts "результат: #{result_nil.inspect}"  # => nil

# 3. вызов метода объекта с аргументами
puts "вызов метода объекта с аргументами:"
result_mult = GemRubyRust.call_obj_multiply(obj, 2, 3)
puts "результат: #{result_mult}"  # => 6

Соответственно, вывод программы:

$ ruby test.rb

call 'a' from module Ruby
123
call 'a' from obj Ruby
nil
call Ruby methods from Rust:
вызов метода модуля:
call 'a' from module Ruby
результат: 246
вызов метода объекта (возврат nil):
call 'a' from obj Ruby
результат: nil
вызов метода объекта с аргументами:
multiply from Ruby: 2 * 3
результат: 6

На этом можно и закончить этот краткий обзор возможностей magnus. И стоит заметить, magnus уже является как бы неотъемлемой частью языка Ruby, судя по тому, что инструмент bundle поддерживает генерацию gem с использованием Rust.

Для более полной и подробной информации рекомендую обратиться к первоисточникам:

Magnus

Magnus (crates.io)

Magnus (GitHub)

serde_magnus

rb_sys

Ruby & Rust: Build fast, memory-safe Ruby extensions with Rust

Я привёл лишь небольшие примеры взаимодействия Ruby и Rust с использованием Magnus. Возможности Magnus значительно более обширные и мощные — тут уж, как говорится, на что хватит ваших знаний, опыта и фантазии.

Я думаю, становится понятно, что это не просто библиотека, очередной механизм обращения одного языка к другому, вызов функций и методов, а это настоящий симбиоз двух очень мощных языков программирования, которые в связке дают инструмент, который позволит решить практически любые задачи. Я не говорю о мобильных приложениях, например, потому как я уже сформировал своё мнение об этой сфере ещё 13 лет назад, и по большому счёту я оказался прав. Сейчас же в симбиозе языков программирования Ruby и Rust я вижу огромный потенциал — красота и удобство для программиста языка Ruby в сочетании с мощностью, скоростью и безопасностью языка Rust предоставляют огромные возможности для реализации программ и систем любого уровня и сложности — бизнес-логика, продуктивное программирование — Ruby, системный уровень, быстродействие, безопасность выполения - Rust. И речь тут как раз не идёт о том, какой язык выбрать, я думаю, что самое логичное и правильное - использовать эти языка оба вместе, в связке, без всяких сравнений и противопоставлений. По своему опыту скажу, что оба языка очень дополняют друг друга и прекрасно "ложаться в голову" для понимания — самое лучшее изучить и знать оба этих языка, использовать их совместно.

Сферы применения связки Ruby + Rust, когда есть необходимость использовать

  • Вычислительно сложные задачи (математика, криптография, нейросети)
  • Программы системного уровня, СУБД, Большие данные
  • Парсеры и обработчики данных, компиляторы
  • Работа с изображениями, видео, аудио
  • Сетевые протоколы
  • Игры и симуляции

Не целевообразно использовать для (Ruby вполне справится):

  • Простой бизнес-логики
  • CRUD-операций
  • Веб-интерфейсов

В 2026 году связка Ruby + Rust стала особенно актуальной

  • YJIT (написан на Rust) ускорил Ruby в 5-7 раз
  • Magnus достиг зрелости и стабильности
  • Rust стал мейнстримом системного программирования
  • Ruby-сообщество активно внедряет Rust для критичных компонентов

Ruby + Rust

август 2026