#ruby #mri #cruby

ruru

Rust 中的原生 Ruby 扩展

22 个版本 (4 个重大更改)

使用旧的 Rust 2015

0.9.3 2017年1月10日
0.9.2 2016年12月10日
0.9.1 2016年11月3日
0.7.8 2016年7月9日

#5#mri

MIT 许可证

150KB
1.5K SLoC

Ruru (Rust + Ruby)

Rust 中的原生 Ruby 扩展

Documentation Build Status Build status Gitter


文档
网站

你是否曾考虑过重写你的一些 慢速 Ruby 应用程序的部分?

只需将你的 Ruby 应用程序逐方法、逐类替换为 Rust,无需更改类接口或任何其他 Ruby 代码。

像 Ruby 一样简单,像 Rust 一样高效。

内容

示例

著名的 String#blank? 方法

Yehuda Katz 编写的快速的 String#blank? 实现

#[macro_use]
extern crate ruru;

use ruru::{Boolean, Class, Object, RString};

methods!(
   RString,
   itself,

   fn string_is_blank() -> Boolean {
       Boolean::new(itself.to_string().chars().all(|c| c.is_whitespace()))
   }
);

#[no_mangle]
pub extern fn initialize_string() {
    Class::from_existing("String").define(|itself| {
        itself.def("blank?", string_is_blank);
    });
}

简单的 Sidekiq 兼容服务器

仓库链接

安全的转换

从 0.8.0 版本开始,对内置 Ruby 类型以及自定义类型提供了安全的转换。

让我们想象我们在编写一个 HTTP 服务器。它应该处理从 Ruby 方面传递过来的请求。

任何响应 #body 方法的对象都被视为有效的请求。

#[macro_use]
extern crate ruru;

use std::error::Error;
use ruru::{Class, Object, RString, VerifiedObject, VM};

class!(Request);

impl VerifiedObject for Request {
    fn is_correct_type<T: Object>(object: &T) -> bool {
        object.respond_to("body")
    }

    fn error_message() -> &'static str {
        "Not a valid request"
    }
}

class!(Server);

methods!(
    Server,
    itself,

    fn process_request(request: Request) -> RString {
        let body = request
            .and_then(|request| request.send("body", vec![]).try_convert_to::<RString>())
            .map(|body| body.to_string());

        // Either request does not respond to `body` or `body` is not a String
        if let Err(ref error) = body {
            VM::raise(error.to_exception(), error.description());
        }

        let formatted_body = format!("[BODY] {}", body.unwrap());

        RString::new(&formatted_body)
    }
);

#[no_mangle]
pub extern fn initialize_server() {
    Class::new("Server", None).define(|itself| {
        itself.def("process_request", process_request);
    });
}

将 Rust 数据包装成 Ruby 对象

Server 包装成 RubyServer 对象

#[macro_use] extern crate ruru;
#[macro_use] extern crate lazy_static;

use ruru::{AnyObject, Class, Fixnum, Object, RString, VM};

// The structure which we want to wrap
pub struct Server {
    host: String,
    port: u16,
}

impl Server {
    fn new(host: String, port: u16) -> Self {
        Server {
            host: host,
            port: port,
        }
    }

    fn host(&self) -> &str {
        &self.host
    }

    fn port(&self) -> u16 {
        self.port
    }
}

wrappable_struct!(Server, ServerWrapper, SERVER_WRAPPER);

class!(RubyServer);

methods!(
    RubyServer,
    itself,

    fn ruby_server_new(host: RString, port: Fixnum) -> AnyObject {
        let server = Server::new(host.unwrap().to_string(),
                                 port.unwrap().to_i64() as u16);

        Class::from_existing("RubyServer").wrap_data(server, &*SERVER_WRAPPER)
    }

    fn ruby_server_host() -> RString {
        let host = itself.get_data(&*SERVER_WRAPPER).host();

        RString::new(host)
    }

    fn ruby_server_port() -> Fixnum {
        let port = itself.get_data(&*SERVER_WRAPPER).port();

        Fixnum::new(port as i64)
    }
);

fn main() {
    let data_class = Class::from_existing("Data");

    Class::new("RubyServer", Some(&data_class)).define(|itself| {
        itself.def_self("new", ruby_server_new);

        itself.def("host", ruby_server_host);
        itself.def("port", ruby_server_port);
    });
}

真正的并行性

Ruru 提供了一种方法,通过释放 GVL (GIL) 来启用 Ruby 线程的真正并行性。

这意味着一个释放了 GVL 的线程可以与其他线程并行运行,而不会被 GVL 打断。

当前示例演示了一个“重”计算(为了简单起见,为 2 * 2)并行运行。

#[macro_use] extern crate ruru;

use ruru::{Class, Fixnum, Object, VM};

class!(Calculator);

methods!(
    Calculator,
    itself,

    fn heavy_computation() -> Fixnum {
        let computation = || { 2 * 2 };
        let unblocking_function = || {};

        // release GVL for current thread until `computation` is completed
        let result = VM::thread_call_without_gvl(
            computation,
            Some(unblocking_function)
        );

        Fixnum::new(result)
    }
);

fn main() {
    Class::new("Calculator", None).define(|itself| {
        itself.def("heavy_computation", heavy_computation);
    });
}

定义一个新类

假设你有一个 Calculator 类。

class Calculator
  def pow_3(number)
    (1..number).each_with_object({}) do |index, hash|
      hash[index] = index ** 3
    end
  end
end

# ... somewhere in the application code ...
Calculator.new.pow_3(5) #=> { 1 => 1, 2 => 8, 3 => 27, 4 => 64, 5 => 125 }

你发现调用 pow_3 对于大数字来说非常慢,因此决定用 Rust 替换整个类。

#[macro_use]
extern crate ruru;

use std::error::Error;
use ruru::{Class, Fixnum, Hash, Object, VM};

class!(Calculator);

methods!(
    Calculator,
    itself,

    fn pow_3(number: Fixnum) -> Hash {
        let mut result = Hash::new();

        // Raise an exception if `number` is not a Fixnum
        if let Err(ref error) = number {
            VM::raise(error.to_exception(), error.description());
        }

        for i in 1..number.unwrap().to_i64() + 1 {
            result.store(Fixnum::new(i), Fixnum::new(i.pow(3)));
        }

        result
    }
);

#[no_mangle]
pub extern fn initialize_calculator() {
    Class::new("Calculator", None).define(|itself| {
        itself.def("pow_3", pow_3);
    });
}

Ruby

# No Calculator class in Ruby anymore

# ... somewhere in the application ...
Calculator.new.pow_3(5) #=> { 1 => 1, 2 => 8, 3 => 27, 4 => 64, 5 => 125 }

类的 API 没有发生变化,因此无需更改应用程序其他地方的任何代码。

只替换几个方法而不是整个类

如果示例中的 Calculator 类有更多的 Ruby 方法,但只想替换 pow_3,请使用 Class::from_existing()

Class::from_existing("Calculator").define(|itself| {
    itself.def("pow_3", pow_3);
});

类定义 DSL

Class::new("Hello", None).define(|itself| {
    itself.const_set("GREETING", &RString::new("Hello, World!").freeze());

    itself.attr_reader("reader");

    itself.def_self("greeting", greeting);
    itself.def("many_greetings", many_greetings);

    itself.define_nested_class("Nested", None).define(|itself| {
        itself.def_self("nested_greeting", nested_greeting);
    });
});

这对应于以下 Ruby 代码

class Hello
  GREETING = "Hello, World".freeze

  attr_reader :reader

  def self.greeting
    # ...
  end

  def many_greetings
    # ...
  end

  class Nested
    def self.nested_greeting
      # ...
    end
  end
end

有关 ClassObject 的更多信息,请参阅文档。

从 Rust 调用 Ruby 代码

获取名为 John 且 18 或 19 岁的 User 的账户余额。

default_balance = 0

account_balance = User
  .find_by(age: [18, 19], name: 'John')
  .account_balance

account_balance = default_balance unless account_balance.is_a?(Fixnum)
#[macro_use]
extern crate ruru;

use ruru::{Array, Class, Fixnum, Hash, Object, RString, Symbol};

fn main() {
    let default_balance = 0;
    let mut conditions = Hash::new();

    conditions.store(
        Symbol::new("age"),
        Array::new().push(Fixnum::new(18)).push(Fixnum::new(19))
    );

    conditions.store(
        Symbol::new("name"),
        RString::new("John")
    );

    // Fetch user and his balance
    // and set it to 0 if balance is not a Fixnum (for example `nil`)
    let account_balance =
        Class::from_existing("User")
            .send("find_by", vec![conditions.to_any_object()])
            .send("account_balance", vec![])
            .try_convert_to::<Fixnum>()
            .map(|balance| balance.to_i64())
            .unwrap_or(default_balance);
}

查看更多示例的 文档

... 以及为什么 FFI 不够用?

  • 不支持原生 Ruby 类型;

  • 无法创建独立的应用程序来单独运行 Ruby 虚拟机;

  • 无法从 Rust 中调用 Ruby 代码;

如何使用它?

警告!该软件包是工作进展中的(WIP);

建议使用 Thermite 钩子,这是一个基于 Rake 的助手,用于构建和分发基于 Rust 的 Ruby 扩展;

要使用 Ruru,请确保您的 Ruby 版本为 2.3.0 或更高;

  1. 您的本地 MRI 复制必须使用 --enable-shared 选项构建。例如,使用 rbenv
CONFIGURE_OPTS=--enable-shared rbenv install 2.3.0
  1. 将 Ruru 添加到 Cargo.toml
[dependencies]
ruru = "0.9.0"
  1. 将库编译为 dylib
[lib]
crate-type = ["dylib"]
  1. 创建一个初始化扩展的函数
#[no_mangle]
pub extern fn initialize_my_app() {
    Class::new("SomeClass");

    // ... etc
}
  1. 构建扩展
$ cargo build --release

或使用 Thermite

$ rake thermite:build
  1. 在 Ruby 端,打开编译后的 dylib 并调用初始化扩展的函数
require 'fiddle'

library = Fiddle::dlopen('path_to_dylib/libmy_library.dylib')

Fiddle::Function.new(library['initialize_my_app'], [], Fiddle::TYPE_VOIDP).call
  1. Ruru 准备就绪 ❤️

欢迎贡献者!

如果您有任何问题,请加入 Ruru 在 Gitter

许可证

MIT 许可证

版权所有 (c) 2015-2016 Dmitry Gritsay

特此授予任何获得此软件及其相关文档副本(以下简称“软件”)的人免费使用软件的权利,不受任何限制,包括但不限于使用、复制、修改、合并、发布、分发、再许可和/或出售软件的副本,并允许软件的提供方使用软件,前提是遵守以下条件

上述版权声明和本许可声明应包含在软件的所有副本或主要部分中。

软件按“原样”提供,不提供任何明示或暗示的保证,包括但不限于适销性、特定用途的适用性和非侵权性。在任何情况下,作者或版权所有者均不对任何索赔、损害或其他责任承担责任,无论此类责任是基于合同、侵权或其他方式,是否源于、因之而产生或与此软件或软件的使用或其他交易有关。

图标由 Github 设计。

依赖项

~74KB