37 个版本

0.9.0 2023 年 12 月 17 日
0.8.4 2022 年 4 月 17 日
0.8.3 2021 年 9 月 7 日
0.8.2 2021 年 2 月 10 日
0.3.1 2018 年 7 月 18 日

#236Rust 模式

Download history 4573/week @ 2024-03-14 4356/week @ 2024-03-21 2938/week @ 2024-03-28 2308/week @ 2024-04-04 4239/week @ 2024-04-11 4093/week @ 2024-04-18 3763/week @ 2024-04-25 3855/week @ 2024-05-02 2881/week @ 2024-05-09 3561/week @ 2024-05-16 2618/week @ 2024-05-23 2698/week @ 2024-05-30 4129/week @ 2024-06-06 2305/week @ 2024-06-13 1797/week @ 2024-06-20 1275/week @ 2024-06-27

9,940 每月下载量
rutie-serde 中使用

MIT 许可证

340KB
4.5K SLoC

Rutie

GitHub Actions Status Ruby 2 Compatible Maintenance GitHub contributors license crates.io version

Rutie —/ro͞oˈˌtī/rOOˈˌtI/rüˈˌtaI/

将 Ruby 集成到您的 Rust 应用程序中。或将 Rust 集成到您的 Ruby 应用程序中。该项目允许您轻松地实现这两者。

强烈建议您阅读此项目的源代码。每个从 Ruby 映射到 src/class/* 以供公共使用的函数都配有 详细的文档 和示例代码。这是使用 Rutie 的最佳方式。在 examples 目录中也有基于此 README 的集成示例。

此项目是

索引

在 Rust 中使用 Ruby

首先将依赖项添加到您的 Cargo.toml 文件中。

[dependencies]
rutie = "0.8.2"

然后在您的Rust程序中,在代码执行路径的开头添加 VM::init(),并开始使用Rutie。

extern crate rutie;

use rutie::{Object, RString, VM};

fn try_it(s: &str) -> String {
    let a = RString::new_utf8(s);

    // The `send` method returns an AnyObject type.
    let b = unsafe { a.send("reverse", &[]) };

    // We must try to convert the AnyObject
    // type back to our usable type.
    match b.try_convert_to::<RString>() {
        Ok(ruby_string) => ruby_string.to_string(),
        Err(_) => "Fail!".to_string(),
    }
}

#[test]
fn it_works() {

    // Rust projects must start the Ruby VM
    VM::init();

    assert_eq!("selppa", try_it("apples"));
}

fn main() {}

注意:目前,在 Linux 中,您需要将 LD_LIBRARY_PATH 设置为指向当前Ruby库所在的目录,在 Mac 中,您需要将 DYLD_LIBRARY_PATH 设置为该信息。您可以使用以下命令获取路径信息

ruby -e "puts RbConfig::CONFIG['libdir']"

这将使您能够运行 cargo testcargo run

运行 cargo test 应该会让这个测试通过。

在 Ruby 中使用 Rust

您可以使用 bundle gem rutie_ruby_example 开始一个Ruby项目,然后进入该目录后运行 cargo init --lib。从gemspec文件中删除TODOs。将Rutie添加到 Cargo.toml 文件中并定义lib类型。

[dependencies]
rutie = {version="xxx"}

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

然后编辑您的 src/lib.rs 文件以编写Rutie代码。

#[macro_use]
extern crate rutie;

use rutie::{Class, Object, RString, VM};

class!(RutieExample);

methods!(
    RutieExample,
    _rtself,

    fn pub_reverse(input: RString) -> RString {
        let ruby_string = input.
          map_err(|e| VM::raise_ex(e) ).
          unwrap();

        RString::new_utf8(
          &ruby_string.
          to_string().
          chars().
          rev().
          collect::<String>()
        )
    }
);

#[allow(non_snake_case)]
#[no_mangle]
pub extern "C" fn Init_rutie_ruby_example() {
    Class::new("RutieExample", None).define(|klass| {
        klass.def_self("reverse", pub_reverse);
    });
}

这是Rust方面的所有内容。当使用 methods! 宏或 extern 函数时,请确保方法名不会与其他任何名称冲突。这就是为什么这个例子使用了前缀 pub_ 的原因。

现在您只需要在Ruby中加载库。将 rutie 钩子添加到您的gemspec或Gemfile中。

# gemspec
spec.add_dependency 'rutie', '~> 0.0.3'

# Gemfile
gem 'rutie', '~> 0.0.3'

然后在您的项目主文件中加载库 lib/rutie_ruby_example.rb

require 'rutie_ruby_example/version'
require 'rutie'

module RutieRubyExample
  Rutie.new(:rutie_ruby_example).init 'Init_rutie_ruby_example', __dir__
end

这就是您从Rust加载Ruby事物的全部内容。现在要写入 test/rutie_ruby_example_test.rb 中的测试。

require "test_helper"

class RutieRubyExampleTest < Minitest::Test
  def test_it_reverses
    assert_equal "selppa", RutieExample.reverse("apples")
  end
end

为了正确测试,您需要在每次修改Rust代码时始终运行 cargo build --release。使用以下命令运行测试

cargo build --release; rake test

或者更好的是,将您的 Rakefile 修改为在每次测试套件运行之前始终运行 cargo build --release。您可以将测试输入更改以证明它失败,因为上面的测试是按原样工作的。

Rust 中的自定义 Ruby 对象

在Rust中创建一个Ruby对象以便可以直接返回给Ruby,只需要几个简单的东西。

以下是来自 FasterPath 的代码示例摘录。

use rutie::types::{ Value, ValueType };
use rutie::{ RString, AnyObject, Object, Class, VerifiedObject };

pub struct Pathname {
    value: Value
}

impl Pathname {
    pub fn new(path: &str) -> Pathname {
        let arguments = [RString::new_utf8(path).to_any_object()];
        let instance = Class::from_existing("Pathname").new_instance(Some(&arguments));

        Pathname { value: instance.value() }
    }

    pub fn to_any_object(&self) -> AnyObject {
        AnyObject::from(self.value())
    }
}

impl From<Value> for Pathname {
    fn from(value: Value) -> Self {
        Pathname { value }
    }
}

impl Object for Pathname {
    #[inline]
    fn value(&self) -> Value {
        self.value
    }
}

impl VerifiedObject for Pathname {
    fn is_correct_type<T: Object>(object: &T) -> bool {
        Class::from_existing("Pathname").case_equals(object)
    }

    fn error_message() -> &'static str {
        "Error converting to Pathname"
    }
}

如果类在Ruby中尚不存在,您需要在生成该类的新的实例之前创建它。此对象现在可以直接从Rust/Rutie返回到Ruby中。 注意,此定义仅是Ruby对象的Rust兼容表示,并未定义任何Ruby方法,这些方法可以从Ruby中使用。

可变参数函数 / 扩展操作符

Rutie中尚未实现集成动态参数数量的首选方式,但您仍然可以以下方式完成。

use rutie::{AnyObject, Array};
use rutie::types::{Argc, Value};
use rutie::util::str_to_cstring;
use rutie::rubysys::class;
use std::mem;

pub extern fn example_method(argc: Argc, argv: *const AnyObject, _rtself: AnyObject) -> AnyObject {
    let args = Value::from(0);

    unsafe {
        let p_argv: *const Value = mem::transmute(argv);

        class::rb_scan_args(
            argc,
            p_argv,
            str_to_cstring("*").as_ptr(),
            &args
        )
    };

    let arguments = Array::from(args);

    let output = // YOUR CODE HERE.  Use arguments as you see fit.

    output.to_any_object()
}

目前,这种代码风格旨在在 methods! 宏之外使用。您可以将此方法放置在类或模块上,就像您通常从 methods! 宏定义中那样。

#[macro_use]
extern crate rutie;

use rutie::{Class, Object, VM};

class!(Example);

// Code from above

fn main() {
    VM::init();
    Class::new("Example", None).define(|klass| {
        klass.def("example_method", example_method);
    });
}

Rutie 项目计划移除编写用于变长参数支持的不安全代码的需求,并可能更新 methods! 宏以原生支持此功能。

从 Ruru 迁移到 Rutie

<0.1

对于使用小于 0.1 版本的 Rutie,更改很简单。将程序中所有 ruru 字符串替换为 rutie。如果您想使用 Rutie 中的 ruby-sys 代码而不是要求 ruby-sys,可以将所有现有的 ruby_sys 引用更改为 rutie::rubysys

0.1

您还需要更改其他类似的内容,例如 Error 已被移除。为此;将类型为 ruru::result::Error 的实例更改为 rutie::AnyException

0.2

已将 parse_argumentsVM 迁移到 util

0.3

内部更改 utilbindingrubysys 已被替换,以减少混淆并减少重复。

安全性 — Rutie 哲学与 Rust 哲学在安全性上的对比

我写这一节是为了指出,截至本文撰写时,Rust 乐于为其 crates 和 Rutie crate 承诺的安全性目前并不一致。典型的 Rust 安全性为包装 C 代码的库是有一个以 -sys 扩展命名的 unsafe crate,然后有一个 crate 来包装它以使其安全。

Rutie 是 Ruru 项目的官方分支,因此该项目的许多设计决策保持不变。Rutie 还引入了 ruby-sys crate,并将其视为内部私有 API/module;但仍然公开供其他开发者使用,以便他们可以完全控制在其之上设计自己的 API。

Rutie 中存在的一个明显违反 Rust 安全哲学的问题是,任何调用 Ruby 代码的方法都可能抛出异常,并且不会返回类型为 Option<AnyObject, AnyException> 的类型,当 Ruby 中抛出异常时将引发恐慌……这会杀死正在运行的应用程序进程。避免恐慌的方法很简单:要么保证您正在运行的 Ruby 代码永远不会抛出异常,要么使用返回类型为 Option<AnyObject, AnyException> 的 "保护" 方法来处理从 Ruby 中抛出的异常。处理 Rust 代码中从 Ruby 抛出的异常。任何人都可以通过阅读和理解此库中 保护 方法的编写方式来实现此安全性,并与之合作。

关于 “为什么每个方法不保证 Rust 所规定的安全性?” 的重要考虑是,Ruby 中的异常处理不是一个零成本的抽象。因此,在需要实现它时会有性能成本。有人可以很容易地争论,安全性的保证比将风险留给缺乏经验的开发者更重要。但也有人可以争论,将性能成本的选择,以及异常捕获偶尔是不必要的这一事实,留给开发者会更好。鉴于设计决策的遗留性在很大程度上被继承,这个项目倾向于后者,即在完全安全与额外的性能之间进行选择的决定权在开发者。

我不反对这个项目实现100%的安全性,但这将是一个重大的变化,并且是一个完全不同的API,需要考虑很多决策。此外,由于该项目并未严格遵循Rust的安全性原则,就像预期的一个crate库应该做的,因此该项目不会达到稳定的1.0版本发布,因为稳定性和安全性是相辅相成的。

我喜欢安全性保证,并且新特性和语言API将尽可能朝着这个方向构建。你可以通过查看已以这种方式实现的功能来了解安全API的设计,例如Enumerator功能(主要封装方法名称和调用protect_send)。

故障排除

某些 Rubies 在 CI 服务器测试中崩溃

有时构建的Ruby二进制文件并不适合该系统。如果这是问题所在,请确保为该系统编译Ruby。使用RVM,使用你选择的Ruby版本执行以下操作:rvm reinstall --disable-binary

Rust 信号:11,SIGSEGV:无效内存引用

这是一个指示,表明您还没有在Rust中启动Ruby VM,使用以下代码:VM::init();在使用Rust中的Ruby代码之前,请先执行此操作。

加载共享库时出错:libruby.so.#.#:无法打开共享对象文件:未找到文件或目录

当Rutie构建尝试与libruby链接时,但未在您的库搜索路径中找到它时,会发生这种情况。如果您正在构建一个调用VM::init();的独立程序,请将其添加到LD_LIBRARY_PATH/DYLD_LIBRARY_PATH,或者如果您正在构建一个要加载到正在运行的Ruby VM中的库,则可以通过设置环境变量NO_LINK_RUTIE或通过在您的Cargo.toml中为Rutie启用cargo功能no-link来禁用链接,如下所示:

[dependencies]
rutie = {version="xxx", features=["no-link"]}

methods! 宏内部调用方法不起作用

该宏的设计不使用您提供的相同的参数签名,因此建议您在Rust中用函数实现任何想要重用的方法,而不是在methods!宏外部。您可以在定义Ruby要使用的方法时,简单地在methods!宏中调用那个新外部方法。

在 Rust 代码中处理 Ruby 抛出的异常

如果您使用任何不返回Result<AnyObject, AnyException>的方法,则Ruby端抛出的任何异常都会干扰该Ruby线程,导致Rust恐慌并停止。Ruby内部使用异常通过内部线程全局值影响整个线程。要处理Ruby可能在Rust代码执行期间抛出异常的地方,您应该使用设计来处理这些情况的方法。

  • VM::eval
  • Object.protect_send
  • Object.protect_public_send

如果您正在编写低级代码并想更直接地与内部Ruby异常交互,您可以使用VM::protect,并阅读Object.protect_send的源代码以了解如何实现。

使用用 C 编写的 Ruby 方法进行 GC 时崩溃

可能引起此问题的一个可能问题是,当您将项目存储在Rust的堆内存中而不是堆栈上时。

导致此问题的一个示例情况如下:

Class::from_existing("Pathname").new_instance(&vec![RString::new_utf8(path).to_any_object()])

Ruby的垃圾回收(GC)从堆栈中跟踪对象。另一方面,Rust的Vec存储元素在堆上。因此,Ruby的GC可能无法找到你创建的字符串并可能释放它。 —— @irxground

为了解决这个问题,需要不使用Vec,而是使用Rust的数组类型来在堆栈上而不是在堆上存储参数。

let arguments = [RString::new_utf8(path).to_any_object()];
Class::from_existing("Pathname").new_instance(&arguments)

操作系统要求

所有内容都是在64位操作系统上使用64位Ruby和Rust构建进行测试的。目前不支持32位。

Ruby 2 注意事项

Ruby 2支持到Rutie的0.8版本。对于Ruby 2的使用,需要安装libssl1.1并在安装时指向它。例如

wget https://www.openssl.org/source/openssl-1.1.1l.tar.gz
tar xf openssl-1.1.1l.tar.gz
cd openssl-1.1.1l
./config --prefix=/usr/local/openssl-1.1.1l --openssldir=/usr/local/openssl-1.1.1l
make
sudo make install
cd ..
rvm install ruby-2.7.7 --with-openssl-dir=/usr/local/openssl-1.1.1l
rvm use 2.7.7

Linux & Mac

  • Rust 1.26或更高版本
  • Ruby(64位)2.5或更高版本

注意:与Ruby 3.0的GC兼容性存在已知问题。GC#markGC#is_markedGC#marked_locations在Ruby 3中不工作。

Windows

  • Rust 1.26或更高版本
  • 使用MingW(64位)构建的Ruby 2.5+
  • MS Visual Studio(构建工具)

动态与静态构建

Ruby需要使用--enable shared选项进行编译。动态链接到Ruby库提供了最佳性能和最佳支持。目前静态构建的支持是不完整的。

如果使用RBENV,则以下建议

CONFIGURE_OPTS=--enable-shared rbenv install 2.7.1

您可以通过运行以下内容来检查您的Ruby是否编译为动态链接,并得到一个"yes"响应。

ruby -e "pp RbConfig::CONFIG['ENABLE_SHARED']"

如果您仍然遇到ld: library not found for -lruby-static问题,请尝试运行cargo clean。这将清除之前尝试的任何工件。

如果您想为添加静态构建支持提出拉取请求,目前有3种方法不支持它,并且需要更新链接到正确的ruby静态lib文件及其路径。

贡献

欢迎贡献者!

代码组织在3个主要层。rubysys文件夹是Ruby C代码的原始映射,并且该文件夹中的所有方法都是不安全的。binding文件夹是我们包装那些方法以将所有不安全方法抽象为安全方法的地方。class文件夹是实现用于Rust代码的Ruby公共API的地方。这些方法必须在文档中记录和测试。在class下有一个名为traits的特质子文件夹。

用于抽象复杂性的宏在src/dsl.rs中。

Ruby的帮助库在子模块文件夹gem中。

Rutie 的未来

Rutie将继续改进,以更好地与Ruby的各个方面兼容。它还将逐步转向Rust的安全性、语义和最佳实践。

我想象一个未来,其中Rutie是帮助Ruby从C切换到Rust的垫脚石。

SemVer

由于这个包将1.0理解为既稳定又安全,并且不太可能发布1.0版本,因此在每个次要版本更新中可能会出现破坏性更改。这些次要版本的破坏性更改将发生在public API的src/class/*src/helpers/*中。对于私有API,每个补丁版本更新都可能包括src/rubysys/*src/binding/*src/util.rs中的破坏性更改。

项目额外历史

如果您需要更多使用示例或git blame历史,请查看Ruru项目,因为Rutie的README已经完全重写,而这个第一个git提交来自Ruru。请注意,其中有一些基本变化,README不会考虑。该项目还合并了ruby-sys,这可能包含一些额外的有益git历史。

LICENSE

这两个合并到该项目中的项目都包含MIT许可证下的标识符。该项目遵循相同的许可。在Cargo.toml文件中,ruby-sys将MIT标记为许可证,而ruru具有该许可证并包含一个LICENSE文件。该项目的LICENSE通过保留MIT许可证的作者行并附加新的作者(这是MIT许可证允许的)来感谢原始作者。

MIT许可证 — 请参阅LICENSE

依赖关系