Ruby / Rust linkage (4) Numerical calculation with Rutie

Introduction

Last time via FFI

\sqrt{ x^2 + y^2 + z^2 }

I called Rust's function to calculate from Ruby. It was a harvest that I found to be surprisingly easy to implement, but the essential speed is Ruby.

Math.sqrt(x * x + y * y + z * z)

It was a pity that it was much slower than calculating. I don't know the cause, but I speculated that the cost of going through FFI was high.

So why not try connecting Rust and Ruby by means other than FFI? This article uses something called Rutie.

Qiita doesn't seem to have an article about Rutie, so I think this is the first article.

In addition, it should be noted.

It was used.

What is Rutie

Official site: danielpclark / rutie: “The Tie Between Ruby and Rust.”

Rutie seems to read like "rooty".

Whereas FFI is a general purpose mechanism that connects multiple languages, Rutie is only between Ruby and Rust. A big feature is that you can write Ruby classes, modules, and methods in Rust. Rust also has types corresponding to Ruby's String, Array, and Hash. Also, it seems that you can call Ruby methods from the Rust side.

Subject

Same as for FFI

\sqrt{ x^2 + y^2 + z^2 }

Write a function that calculates the above in Rust and call it in Ruby.

In the case of FFI, it was like assigning a Rust function to a Ruby method, but in the case of Rutie, it is like writing a Ruby method directly in Rust.

Implementation: Rust side

As with FFI, I will write it so that even people who are not familiar with Rust can reproduce it. However, it is assumed that Rust has already been installed.

Project creation

First at the terminal

cargo new my_rutie_math --lib

And make one Rust project.

my_rutie_math is the project name. A directory with the same name will be created, in which the initial set of files will be stored.

--lib is a specification that" I will make a library crate ".

Editing Cargo.toml

Cargo.toml in the project root ends with [dependencies], which is as follows.

Cargo.toml


[dependencies]
rutie = "0.7.0"

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

(Addition 2020-10-01) The latest version of rutie as of 2020-10-01 is 0.8.1, so if you want to try it from now on

rutie = "0.8.1"

Please. [dependencies]

[dependencies] is the specification of the dependency crate. In Ruby, it's like specifying a dependent gem in a Gemfile. Here, it says that we will use the crate rutie. " 0.7.0 " is the version specification of the rutie crate, but it does not mean "version 0.7.0 or more" but "version 0.7.0 or more and less than 0.8.0". In other words, it is the same as specifying " ~> 0.7.0 " in Ruby's Gemfile.

As of September 4, 2020, the latest release of the rutie crate was version 0.8.0, but for some reason 0.8.0 didn't work [^ dame], so I'll postpone the cause investigation and go one older 0.7. Let's move on with .0.

[^ dame]: I tried it on macOS and msvc and gnu on Windows, but I get an error at the linking stage at compile time. I haven't investigated it in detail, but if I have a chance, I would like to summarize it in another article.

Please note that the explanation on the official website is written on the premise of 0.8.0.

(Addition 2020-10-01) After that, when I tried 0.8.0 again on macOS, there was no particular problem. 0.8.1 was also okay. Maybe something has changed. Please let me know if you get an error in the build.

[lib]

I'm not sure what the next [lib] means.

crate-type literally specifies the type of crate. Of the binary crate and the library crate, the library crate is created, but it seems that there are actually different types of library crates. The following articles are useful.

I tried to summarize the crate_type of Rust --Qiita

cdylib seems to mean a dynamic library for other languages (that is, languages other than Rust). Well, does dy mean dynamic and c means C language?

Module and method description

Then, the description of the main body.

Rutie allows you to create Ruby modules and classes. This time, I just want to create one function (method), so let's make it a module instead of a class. The module name should be MyMath. Name the method hypot3 and define it as a singular method of MyMath. The policy has been set.

Make the file src / lib.rs as follows. Originally, the test code template is written, but you can delete it.

src/lib.rs


#[macro_use]
extern crate rutie;

use rutie::{Object, Module, Float};

module!(MyMath);

methods!(
    MyMath,
    _rtself,

    fn pub_hypot3(x: Float, y: Float, z: Float) -> Float {
        let x = x.unwrap().to_f64();
        let y = y.unwrap().to_f64();
        let z = z.unwrap().to_f64();
        Float::new((x * x + y * y + z * z).sqrt())
    }
);

#[allow(non_snake_case)]
#[no_mangle]
pub extern "C" fn Init_MyMath() {
    Module::new("MyMath").define(|module| {
        module.def_self("hypot3", pub_hypot3)
    });
}

The amount of description is slightly larger than the FFI version.

macro

First, pay attention to module! And methods!. These are macros defined in the rutie crate, which seem to define Ruby modules and methods.

To use these macros, at the beginning

#[macro_use]
extern crate rutie;

(I don't know).

Object, Module, Float

In Rutie, it seems that Rust types corresponding to classes such as Object, Module, Class, Array, Float, Hash, and Symbol of Ruby are defined with ** same name **. However, Ruby's String is named RString instead of the same name. I guess I added R so that it wouldn't overlap with Rust's String.

This time, we need three of these, Object, Module, and Float.

use rutie::{Object, Module, Float};

I write that.

Module definition

How to make a Ruby module called MyMath

module!(MyMath);

Write. Probably the module is not actually created here, but when you execute Module :: new ("MyMath") which appears later.

Method definition

The method definition gives the methods! Macro three arguments. The first argument is the module name MyMath. I don't understand the second argument _rtself at all.

The definition of the function is given in the third argument. Let's extract:

fn pub_hypot3(x: Float, y: Float, z: Float) -> Float {
    let x = x.unwrap().to_f64();
    let y = y.unwrap().to_f64();
    let z = z.unwrap().to_f64();
    Float::new((x * x + y * y + z * z).sqrt())
}

First of all, the function name is prefixed with pub_, which is similar to the code example on the Rutie site, which says" pub_is prefixed so that the function name does not overlap with others. " It is not necessary to attach it if it is clear that it will not be worn. Please be assured thatpub_hypot3 will be the method name hypot3` on the Ruby side.

By the way, both the argument and the return value are of type Float instead of f64. Float seems to be defined here: https://github.com/danielpclark/rutie/blob/v0.7.0/src/class/float.rs

The comments written here can be found as documentation below: https://docs.rs/rutie/0.7.0/rutie/struct.Float.html

A big question arose here. Where to convert from an argument to f64

x.unwrap().to_f64()

I'm trying. According to the previous document, Float should be able to be converted to f64 with to_f64 (). Why are you biting ʻunwrap ()? Hi, the type of this x` is

std::result::Result<rutie::Float, rutie::AnyException>

Seems to be. Probably, when you get the value from Ruby, you may be passed something strange, so it will be Result. So ʻunwrap ()will retrieve a value of typeFloat`. But! The type of function is

fn pub_hypot3(x: Float, y: Float, z: Float) -> Float

Wasn't it? It's Float, x is. Ah ~?

I couldn't find out by my ability even if I looked it up. However, I thought that the point might be that this part is an argument of the methods! Macro. Yes, this function definition **-like thing ** is something that is passed to the macro. It's not the Rust function itself.

Let's put this problem aside and move on. In the function body

let x = x.unwrap().to_f64();

It is said. It is the so-called shadowing that defines x with the same name even though there is x in the argument. The original x is no longer needed anymore, so use a variable with the same name.

Last

Float::new((x * x + y * y + z * z).sqrt())

Is generating a Float type value to be returned to Ruby based on the calculated f64 value.

Definition of initialization function

The term "initialization function" is something I have come up with and may not be appropriate. Anyway, define one function to call from Ruby side. By doing this, I think that the Ruby modules and methods defined in Rust can actually be used on the Ruby side.

Excerpted below.

#[allow(non_snake_case)]
#[no_mangle]
pub extern "C" fn Init_mymath() {
    Module::new("MyMath").define(|module| {
        module.def_self("hypot3", pub_hypot3)
    });
}

I don't know the naming convention of the function name, but I made it in the format of ʻInit_XXXXaccording to the code example. The word# [allow (non_snake_case)]` at the beginning probably means that the compiler is nailed, saying, "Don't complain because it was intentional not to use the snake case."

# [no_mangle] is a familiar spell that the library puts when defining a function to show to the outside, and it seems that the function name cannot be referenced by that name without it.

As for the contents of the function, it seems that the module MyMath is first created withModule :: new ("MyMath"), and the method is created with def_self for it. The argument of define is

|module| {
    module.def_self("hypot3", pub_hypot3)
}

It is in the form of. This is called a closure. It's interesting that the syntax is very similar to Ruby blocks. The difference is that the part corresponding to the block parameter of Ruby is outside the {}. Ruby blocks are not values (not objects), but Rust closures are values and can be passed as function arguments.

module.def_self ("hypot3 ", pub_hypot3) seems to mean that the previously defined pub_hypot3 is spawned in the module MyMath as a method named hypot3.

Now the implementation on the Rust side is okay. There were some things I didn't understand, and it felt a bit confusing. But wouldn't it be nice if Ruby classes, modules and methods could be defined with this level of complexity?

compile

In the project root directory

cargo build --release

I will do it. Then, the deliverable can be in the path target / release / libmy_rutie_math.dylib. However, the extension should be different depending on the target. I wonder if it will be .dll on Windows.

(Addition) When compiling

warning: `extern` fn uses type `MyMath`, which is not FFI-safe
 --> src/lib.rs:9:5
  |
9 |     MyMath,
  |     ^^^^^^ not FFI-safe

Is displayed. (The version of Ruby and Rust was written at the beginning of the article)

The name MyMath seems to say" not FFI-safe ". I don't know what that means, but it's a warning, not an error, so I'll ignore it for now.

(Addition 2020-10-01) The warning not FFI-safe came out in Rust 1.46. It was resolved in Rutie 0.8.1. https://github.com/danielpclark/rutie/issues/128

Implementation: Ruby side

On the Ruby side, we use a gem called rutie. The same name as the crate used in Rust. Easy to understand.

The following sample script is written assuming that it exists in the root directory of the Rust project.

gem "rutie", "~> 0.0.4"
require "rutie"

Rutie.new(:my_rutie_math, lib_path: "target/release").init "Init_mymath", __dir__

p MyMath.hypot3(1.0, 2.0, 3.0)
# => 3.7416573867739413

#reference
p Math.sqrt(1 * 1 + 2 * 2 + 3 * 3)
# => 3.7416573867739413

In the sample above, I tried to show it in one file, and suddenly I did gem" rutie "," ~> 0.0.4 ", but usually I would write it in Gemfile.

Now, how to use rutie gem, first of all, it seems to create a Ruby object with Rutie.new. I wrote : my_rutie_math in the first argument, which is the name of the library created by Rust.

In this article, the project name given when you first cargo new is used as the library name. But at [lib] in Cargo.toml

Cargo.toml


[lib]
name = "hoge"

If you give name like this, it should be the library name. And that should be reflected in the filename of the compiled artifact.

The optional argument lib_path will be discussed later.

Anyway, call ʻinitof the resulting Rutie object. The first argument," Init_mymath "`, is the name of what I tentatively called the "initialization function". The second argument will be touched on shortly.

Anyway, doing this ʻinitfinds the library filelibmy_rutie_math.dylib that Rutie should use and calls the ʻInit_mymath function. Again, the extension of this file depends on the target. Rutie thinks about it and finds it.

So, it's a place to find it, but it's a little confusing. First, based on the second argument of ʻinit, it is seen that it is moved by the relative path given to lib_path`.

For this article, I put the Ruby script in the root of my Rust project, so __dir __ is there. So, the file is found in target / release seen from there.

If you do not give lib_path or any other option, it will be"../ target / release". In this case, this is inconvenient, so I specified lib_path.

use

Easy to use. As per the code example. The MyMath module has a singular method hypot3, so just call it normally. For confirmation, Math.sqrt (1 * 1 + 2 * 2 + 3 * 3) is also displayed, but the same value was obtained.

However, there is one caveat. We have decided (on the Rust side) that all three arguments to hypot3 are Float. What if you give MyMath.hypot3 an Integer object?

I tried it. Dead. It's a so-called panic. If you feed anything other than Float, you will die at x.unwrap (). Of course, on the Rust side, instead of suddenly ʻunwrap (), you can make a function that does not die if you divide it into cases with ʻOk and ʻErr`. Alternatively, on the Ruby side, there is no problem if you typecast to Float and call it.

Benchmark test

Last time (Ruby / Rust linkage (3) Numerical calculation with FFI), I tried hypot3 in a way that uses FFI directly, and" Rust It was much faster to write in Ruby than to call. "

How about the Rutie version? Let's do it without much expectation.

Test code

This time as well, we will measure using a gem called benchmark_driver.

in advance

gem i benchmark_driver

And install it. (It's often confusing, but the gem name is underscore, not hyphen)

This time, unlike the last time, I will write the test code in Ruby.

One thing to keep in mind is that in the sample code above, we used __dir__ to represent ** here **, but if you write it in benchmark_driver, it will be generated by benchmark_driver instead of the location of the benchmark program. It means that the temporary files are located and the Rust library cannot be found.

In the code below, I devised it.

require "benchmark_driver"

Benchmark.driver do |r|
  r.prelude <<~EOT
    gem "rutie", "~> 0.0.4"
    require "rutie"

    Rutie.new(:my_rutie_math, lib_path: "target/release").init "Init_mymath", "#{__dir__}"
  EOT

  r.report "MyMath.hypot3(1.0, 2.0, 3.0)"
  r.report "Math.sqrt(1.0 * 1.0 + 2.0 * 2.0 + 3.0 * 3.0)"
end

prelude writes what to do prior to measurement. report writes the process you want to measure.

Test run

When you run the script above:

Math.sqrt(1.0 * 1.0 + 2.0 * 2.0 + 3.0 * 3.0):  11796989.3 i/s
                MyMath.hypot3(1.0, 2.0, 3.0):   5684591.1 i/s - 2.08x  slower

It's a terrible defeat. The execution speed of the Rutie version is almost the same as the previous FFI version. It takes twice as long as writing in Ruby.

Um, can I sleep today? Next, let Rust do the heavier processing and reveal the nose of the Ruby script.

Recommended Posts

Ruby / Rust linkage (4) Numerical calculation with Rutie
Ruby / Rust linkage (5) Numerical calculation with Rutie ② Bezier
Ruby / Rust linkage (3) Numerical calculation with FFI
Ruby / Rust linkage (6) Extraction of morphemes
Object-oriented numerical calculation
Conditional numerical calculation
Install Ruby 3.0.0 with asdf
Ruby / Rust cooperation (1) Purpose
Ruby score calculation program
Getting Started with Ruby
Ruby / Rust cooperation (2) Means
11th, Classification with Ruby
Evolve Eevee with Ruby