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
63,392 每月下载次数
用于 2 crates
1MB
14K SLoC
Magnus
Rust的高级Ruby绑定。在Rust中编写Ruby扩展gem,或从Rust二进制文件中调用Ruby代码。
示例
定义方法
使用Magnus,可以将常规Rust函数绑定到Ruby作为方法,并自动进行类型转换。调用者传递错误参数或不兼容类型时,将得到与Ruby内置方法相同的ArgumentError
或TypeError
。
定义一个函数(没有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
gem 与 rake-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.rb
和 Rakefile
的示例,请参阅 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 |
---|---|
i8 ,i16 ,i32 ,i64 ,isize ,magnus::Integer |
Integer ,#to_int |
u8 ,u16 ,u32 ,u64 ,usize |
Integer ,#to_int |
f32 ,f64 ,magnus::Float |
Float ,Numeric |
String ,PathBuf ,char ,magnus::RString ,bytes::Bytes *** |
String ,#to_str |
magnus::Symbol |
Symbol ,#to_sym |
bool |
任何对象 |
magnus::Range |
Range |
magnus::Encoding ,magnus::RbEncoding |
Encoding ,编码名称作为字符串 |
Option<T> |
T 或 nil |
(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::SystemTime ,magnus::Time |
时间 |
magnus::值 |
任何对象 |
Vec<T> * |
[T] ,#to_ary |
HashMap<K,V> * |
{K => V} ,#to_hash |
&T ,typed_data::Obj<T> 其中 T: TypedData ** |
实现 <T as TypedData>::class() |
** 当转换为 Vec
和 HashMap
时,T、
K、
V
的类型必须是原生Rust类型。
** 请参阅wrap
宏。
*** 当启用 bytes
功能时
Rust 返回/传递值到 Ruby
有关详细信息,请参阅magnus::IntoValue
,以及magnus::method::ReturnValue
和magnus::ArgList
以获取更多信息。
从 Rust 返回/在 Rust 中调用 Ruby | 在 Ruby 中接收 |
---|---|
i8 ,i16 ,i32 ,i64 ,isize |
整数 |
u8 ,u16 ,u32 ,u64 ,usize |
整数 |
f32 ,f64 |
浮点数 |
String ,&str ,char ,&Path ,PathBuf |
字符串 |
bool |
true /false |
() |
nil |
Range ,RangeFrom ,RangeTo ,RangeInclusive |
Range |
Option<T> |
T 或 nil |
Result<T, magnus::Error> (仅返回) |
T 或引发错误 |
(T, U) ,(T, U, V) 等,[T; N] ,Vec<T> |
数组 |
HashMap<K,V> |
哈希表 |
std::时间::SystemTime |
时间 |
T ,typed_data::Obj<T> 其中T: TypedData ** |
实现 <T as TypedData>::class() |
** 请参阅wrap
宏。
通过Serde进行转换
Rust类型也可以使用Serde和serde_magnus
crate转换为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堆分配的数据结构中,例如Vec
、HashMap
或Box
。这必须不惜一切代价避免。
虽然可以将任何可能暴露此不安全性的函数标记为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