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.
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.
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.
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.
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 ".
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?
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.
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.
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.
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 that
pub_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 type
Float`.
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.
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?
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
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 file
libmy_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
.
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.
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.
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.
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