Взаимодействие 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 есть команда bundle gem <имя> --ext=rust (bundle gem), которая генерирует gem Ruby (библиотеку для использования в программах Ruby) сразу же с шаблоном вызова Rust из Ruby, используя крейт magnus.
Сгенерируем такой gem, назовём его gem_ruby_rust:
$ bundle 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.md) bundle 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.
Для более полной и подробной информации рекомендую обратиться к первоисточникам:
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 для критичных компонентов
август 2026