#ruby #extension #value-object #gem #api-bindings #rubygem

magnus

高级Ruby绑定。在Rust中编写Ruby扩展gem,或从Rust二进制文件中调用Ruby代码。

24个版本

0.7.1 2024年6月30日
0.6.4 2024年5月9日
0.6.2 2023年9月19日
0.6.0 2023年7月29日
0.1.0 2022年2月26日

#9 in FFI

Download history 16271/week @ 2024-05-02 16258/week @ 2024-05-09 16242/week @ 2024-05-16 12154/week @ 2024-05-23 14499/week @ 2024-05-30 21526/week @ 2024-06-06 21585/week @ 2024-06-13 19341/week @ 2024-06-20 17264/week @ 2024-06-27 11438/week @ 2024-07-04 19478/week @ 2024-07-11 13391/week @ 2024-07-18 20261/week @ 2024-07-25 15294/week @ 2024-08-01 14142/week @ 2024-08-08 11537/week @ 2024-08-15

63,392 每月下载次数
用于 2 crates

MIT 协议

1MB
14K SLoC

Magnus

Rust的高级Ruby绑定。在Rust中编写Ruby扩展gem,或从Rust二进制文件中调用Ruby代码。

API文档 | GitHub | crates.io

入门 | 类型转换 | 安全性 | 兼容性

示例

定义方法

使用Magnus,可以将常规Rust函数绑定到Ruby作为方法,并自动进行类型转换。调用者传递错误参数或不兼容类型时,将得到与Ruby内置方法相同的ArgumentErrorTypeError

定义一个函数(没有Ruby self 参数)

fn fib(n: usize) -> usize {
    match n {
        0 => 0,
        1 | 2 => 1,
        _ => fib(n - 1) + fib(n - 2),
    }
}

#[magnus::init]
fn init(ruby: &magnus::Ruby) -> Result<(), Error> {
    ruby.define_global_function("fib", magnus::function!(fib, 1));
    Ok(())
}

定义一个方法(有Ruby self 参数)

fn is_blank(rb_self: String) -> bool {
    !rb_self.contains(|c: char| !c.is_whitespace())
}

#[magnus::init]
fn init(ruby: &magnus::Ruby) -> Result<(), Error> {
    // returns the existing class if already defined
    let class = ruby.define_class("String", ruby.class_object())?;
    // 0 as self doesn't count against the number of arguments
    class.define_method("blank?", magnus::method!(is_blank, 0))?;
    Ok(())
}

调用Ruby方法

一些Ruby方法在Ruby的C API和Magnus中有直接对应的方法。Ruby的Object#frozen?方法可以作为magnus::ReprValue::check_frozen使用,或者Array#[]变为magnus::RArray::aref

仅在Ruby中定义的其他Ruby方法必须使用 magnus::ReprValue::funcall 来调用。Magnus的所有Ruby包装类型都实现了 ReprValue 特性,因此可以在它们上使用 funcall

let s: String = value.funcall("test", ())?; // 0 arguments
let x: bool = value.funcall("example", ("foo",))?; // 1 argument
let i: i64 = value.funcall("other", (42, false))?; // 2 arguments, etc

funcall 将转换返回类型,如果类型转换失败或方法调用引发了错误,则返回 Err(magnus::Error)。要跳过类型转换,请确保返回类型是 magnus::Value

在Ruby对象中包装Rust类型

Rust结构体和枚举可以被包装成Ruby对象,以便可以返回到Ruby。

类型可以通过使用 magnus::wrap 宏(或实现 magnus::TypedData)来选择加入此功能。每当返回兼容类型到Ruby时,它将被包装在指定的类中,当它被传递回Rust时,它将解包为引用。

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

#[magnus::wrap(class = "Point")]
struct Point {
    x: isize,
    y: isize,
}

impl Point {
    fn new(x: isize, y: isize) -> Self {
        Self { x, y }
    }

    fn x(&self) -> isize {
        self.x
    }

    fn y(&self) -> isize {
        self.y
    }

    fn distance(&self, other: &Point) -> f64 {
        (((other.x - self.x).pow(2) + (other.y - self.y).pow(2)) as f64).sqrt()
    }
}

#[magnus::init]
fn init(ruby: &Ruby) -> Result<(), Error> {
    let class = ruby.define_class("Point", ruby.class_object())?;
    class.define_singleton_method("new", function!(Point::new, 2))?;
    class.define_method("x", method!(Point::x, 0))?;
    class.define_method("y", method!(Point::y, 0))?;
    class.define_method("distance", method!(Point::distance, 1))?;
    Ok(())
}

如果需要可变性,可以使用newtype模式或 RefCell

struct Point {
    x: isize,
    y: isize,
}

#[magnus::wrap(class = "Point")]
struct MutPoint(std::cell::RefCell<Point>);

impl MutPoint {
    fn set_x(&self, i: isize) {
        self.0.borrow_mut().x = i;
    }
}

要允许包装类型被继承,它们必须实现 Default,并定义alloc func和初始化方法

#[derive(Default)]
struct Point {
    x: isize,
    y: isize,
}

#[derive(Default)]
#[wrap(class = "Point")]
struct MutPoint(RefCell<Point>);

impl MutPoint {
    fn initialize(&self, x: isize, y: isize) {
        let mut this = self.0.borrow_mut();
        this.x = x;
        this.y = y;
    }
}

#[magnus::init]
fn init(ruby: &Ruby) -> Result<(), Error> {
    let class = ruby.define_class("Point", ruby.class_object()).unwrap();
    class.define_alloc_func::<MutPoint>();
    class.define_method("initialize", method!(MutPoint::initialize, 2))?;
    Ok(())
}

入门指南

编写扩展gem(从Ruby调用Rust)

Ruby扩展必须构建为动态系统库,这可以通过在您的 Cargo.toml 中设置 crate-type 属性来完成。

Cargo.toml

[lib]
crate-type = ["cdylib"]

[dependencies]
magnus = "0.7"

当Ruby加载您的扩展时,它会调用在您的扩展中定义的 'init' 函数。在这个函数中,您需要定义您的Ruby类并将Rust函数绑定到Ruby方法上。使用 #[magnus::init] 属性来标记您的init函数,以便它可以正确地暴露给Ruby。

src/lib.rs

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

fn distance(a: (f64, f64), b: (f64, f64)) -> f64 {
    ((b.0 - a.0).powi(2) + (b.1 - a.1).powi(2)).sqrt()
}

#[magnus::init]
fn init(ruby: &Ruby) -> Result<(), Error> {
    ruby.define_global_function("distance", function!(distance, 2));
}

如果您希望将您的扩展打包为Gem,我们建议使用 rb_sys gemrake-compiler 一起构建。这些工具将自动构建您的Rust扩展为动态库,并将其打包为gem。

注意:最新的rubygems版本已经对编译Rust有beta支持,所以将来将不再需要 rb_sys gem。

my_example_gem.gemspec

spec.extensions = ["ext/my_example_gem/extconf.rb"]

# needed until rubygems supports Rust support is out of beta
spec.add_dependency "rb_sys", "~> 0.9.39"

# only needed when developing or packaging your gem
spec.add_development_dependency "rake-compiler", "~> 1.2.0"

然后,我们在 ext 目录中添加一个 extconf.rb 文件。Ruby会在编译过程中执行此文件,并在 ext 目录中生成一个 Makefile。有关更多信息,请参阅 rb_sys gem

ext/my_example_gem/extconf.rb

require "mkmf"
require "rb_sys/mkmf"

create_rust_makefile("my_example_gem/my_example_gem")

有关 extconf.rbRakefile 的示例,请参阅 rust_blank 示例。运行 rake compile 将将扩展放置在 lib/my_example_gem/my_example_gem.so(或在macOS上为 .bundle),您可以从Ruby像这样加载它

lib/my_example_gem.rb

require_relative "my_example_gem/my_example_gem"

要查看更详细的示例(包括交叉编译等),请参阅 rb-sys 示例项目。虽然 lib.rs 中的代码没有使用 magnus,但它可以正确编译和运行。

Rust 中嵌入 Ruby

要从 Rust 程序中调用 Ruby,启用 embed 功能

Cargo.toml

[dependencies]
magnus = { version = "0.7", features = ["embed"] }

这允许链接到 Ruby 并访问 embed 模块。必须在调用 Ruby 之前调用 magnus::embed::init,并且返回的值必须在您完成 Ruby 之前不能被丢弃。init 不能被多次调用。

src/main.rs

use magnus::eval;

fn main() {
    magnus::Ruby::init(|ruby| {
        let val: f64 = eval!(ruby, "a + rand", a = 1)?;

        println!("{}", val);

        Ok(())
    }).unwrap();
}

类型转换

Magnus 将自动在 Rust 和 Ruby 类型之间进行转换,包括将 Ruby 异常转换为 Rust Result 以及反向转换。

这些转换遵循 Ruby 的核心和标准库中设定的模式,其中许多转换将委托给一个 #to_<type> 方法,如果对象不是请求的类型,但实现了该 #to_<type> 方法。

以下表格概述了许多常见的转换。有关类型完整列表,请参阅 Magnus API 文档。

Rust 函数接受 Ruby 值

有关更多详细信息,请参阅 magnus::TryConvert

Rust 函数参数 接受来自 Ruby
i8i16i32i64isizemagnus::Integer Integer#to_int
u8u16u32u64usize Integer#to_int
f32f64magnus::Float FloatNumeric
StringPathBufcharmagnus::RStringbytes::Bytes*** String#to_str
magnus::Symbol Symbol#to_sym
bool 任何对象
magnus::Range Range
magnus::Encodingmagnus::RbEncoding Encoding,编码名称作为字符串
Option<T> Tnil
(T, U)(T, U, V) [T, U][T, U, V]等,#to_ary
[T;N] [T]#to_ary
magnus::RArray Array#to_ary
magnus::RHash Hash#to_hash
std::time::SystemTimemagnus::Time 时间
magnus:: 任何对象
Vec<T>* [T]#to_ary
HashMap<K,V>* {K => V}#to_hash
&Ttyped_data::Obj<T> 其中 T: TypedData** 实现 <T as TypedData>::class()

** 当转换为 VecHashMap 时,TKV的类型必须是原生Rust类型。

** 请参阅wrap宏。

*** 当启用 bytes 功能时

Rust 返回/传递值到 Ruby

有关详细信息,请参阅magnus::IntoValue,以及magnus::method::ReturnValuemagnus::ArgList以获取更多信息。

从 Rust 返回/在 Rust 中调用 Ruby 在 Ruby 中接收
i8i16i32i64isize 整数
u8u16u32u64usize 整数
f32f64 浮点数
String&strchar&PathPathBuf 字符串
bool true/false
() nil
RangeRangeFromRangeToRangeInclusive Range
Option<T> Tnil
Result<T, magnus::Error>(仅返回) T 或引发错误
(T, U)(T, U, V)等,[T; N]Vec<T> 数组
HashMap<K,V> 哈希表
std::时间::SystemTime 时间
Ttyped_data::Obj<T>其中T: TypedData** 实现 <T as TypedData>::class()

** 请参阅wrap宏。

通过Serde进行转换

Rust类型也可以使用Serdeserde_magnuscrate转换为Ruby,反之亦然。

手动转换

可能存在需要绕过自动类型转换的情况,为此请使用类型magnus::Value,然后从那里手动转换或类型检查。

例如,如果您想确保您的函数始终传递一个UTF-8编码的String,以便您可以不进行分配就获取引用,可以执行以下操作

fn example(ruby: &Ruby, val: magnus::Value) -> Result<(), magnus::Error> {
    // checks value is a String, does not call #to_str
    let r_string = RString::from_value(val)
        .ok_or_else(|| magnus::Error::new(ruby.exception_type_error(), "expected string"))?;
    // error on encodings that would otherwise need converting to utf-8
    if !r_string.is_utf8_compatible_encoding() {
        return Err(magnus::Error::new(
            ruby.exception_encoding_error(),
            "string must be utf-8",
        ));
    }
    // RString::as_str is unsafe as it's possible for Ruby to invalidate the
    // str as we hold a reference to it. The easiest way to ensure the &str
    // stays valid is to avoid any other calls to Ruby for the life of the
    // reference (the rest of the unsafe block).
    unsafe {
        let s = r_string.as_str()?;
        // ...
    }
    Ok(())
}

安全性

在Rust代码中使用Magnus时,Ruby对象必须保持在栈上。如果对象移动到堆上,Ruby GC无法访问它们,它们可能会被垃圾回收。这可能导致内存安全问题。

无法在Rust的类型系统中或通过借用检查器强制执行此规则,Magnus的用户必须手动维护此规则。

违反此规则的一个示例是将Ruby对象存储在Rust堆分配的数据结构中,例如VecHashMapBox。这必须不惜一切代价避免。

虽然可以将任何可能暴露此不安全性的函数标记为unsafe,但这意味着几乎与Ruby的所有交互都会是unsafe。这将没有区分真正需要更多小心使用的真正不安全函数的方法。

除了这一点,Magnus力求为库的用户提供Rust的通常安全性保证。Magnus本身包含大量标记有unsafe关键字的代码,不使用它就无法与Ruby的C-api进行交互,但Magnus的用户应该能够在不需要使用unsafe的情况下做大部分事情。

兼容性

完全支持Ruby版本3.0、3.1、3.2和3.3。

Magnus目前与Ruby 2.7兼容,并且仍然针对它进行测试,但因为这个语言版本不再由Ruby开发者支持,所以不推荐使用,并且在Magnus中未来支持不能保证。

Ruby绑定将在编译时生成,这可能需要安装libclang。

当前支持的最低Rust版本是Rust 1.61。

通过低级rb-syscrate提供对静态链接Ruby的支持,可以通过将以下内容添加到您的Cargo.toml中启用

# * should select the same version used by Magnus
rb-sys = { version = "*", default-features = false, features = ["ruby-static"] }

rb-sys支持跨平台编译(请参阅此处列出的平台)

Magnus尚未在32位系统上进行测试。我们努力确保它能够编译。欢迎提交补丁。

与Magnus一起工作的crates

rb-sys

Magnus 使用 rb-sys 来提供 Ruby 的底层绑定。`rb-sys` 功能启用 rb_sys 模块,用于与 rb-sys 进行高级互操作性,允许您访问 Magnus 没有公开的低级 Ruby API。

serde_magnus

serde_magnus 将 Serde 和 Magnus 集成,用于无缝地将 Rust 数据结构序列化和反序列化为 Ruby,反之亦然。

用户

  • halton 是一个 Ruby 晒库,提供了一种高度优化的方法来生成 Halton 序列。

如果您想将您的项目列在这里,请提交一个 pull request

故障排除

静态链接问题

如果您在嵌入静态 Ruby 时遇到如 symbol not found in flat namespace 'rb_ext_ractor_safe' 这样的错误,您需要指导 Cargo 不要删除它认为的死代码。

在您的 Cargo.toml 文件相同的目录下,创建一个 .cargo/config.toml 文件,并包含以下内容

[build]
# Without this flag, when linking static libruby, the linker removes symbols
# (such as `_rb_ext_ractor_safe`) which it thinks are dead code... but they are
# not, and they need to be included for the `embed` feature to work with static
# Ruby.
rustflags = ["-C", "link-dead-code=on"]

命名

Magnus 以《战锤 40,000》宇宙中的角色 Magnus the Red 命名。他是一个相信能够驯服跨维空间精神能量的巫师。最终,他的傲慢导致他堕入混沌,但愿使用这个库能给您带来更好的结果。

许可证

本项目受 MIT 许可证许可,请参阅 LICENSE。

依赖

~0.4–3MB
~54K SLoC