Series of articles table of contents
This time, an example of a gem with Rust code in the source. The following articles will be helpful. Easy to make with Rust Ruby Gem --Qiita
In the above article, Rust was compiled during development and the product was included in the gem package. On the other hand, let's take ** how to compile (build) Rust when installing gem **. To do this, of course, Rust must be installed in the installation environment.
We will expose the prototype gem to GitHub. The gem name is rust_gem_sample01. https://github.com/scivola/rust_gem_sample01
It's a prototype to show how to do it, so I haven't included it in RubyGems.org.
I intended to install it on macOS, Linux, and Windows.
This gem is not for practical purposes, so a simple example. Ruby's built-in methods and standard attached libraries do not have a random number generation function that has a distribution other than a uniform distribution, so let's do that. For the time being, the one that returns a random number with a normal distribution.
Things that can be used like this:
require "rust_gem_sample01"
standard_deviation = 2.0
p RustGemSample01.rand_norm(standard_deviation)
# => -3.1674786173729506
Define singular method rand_norm
in RustGemSample01 module. If the standard deviation is given to the argument, a random number with an average value of 0 is returned.
The normal distribution has two parameters, the mean value and the standard deviation, but if you want to change the mean value, you only have to add them, so the argument is only the standard deviation. The more arguments there are, the higher the cost of FFI, so I think this is fine.
It is not listed in RubyGems.org, so
gem install rust_gem_sample01
Cannot be installed.
Follow the steps below to clone the repository and build and install the gem. I would like to know if it can be installed properly in various environments, so I would appreciate your cooperation.
git clone https://github.com/scivola/rust_gem_sample01.git
cd rust_gem_sample01
rake install
This final rake install
is to create a gem package from the gem's project file [^ mkpkg] and install the gem on your system (ie globally).
[^ mkpkg]: The gem package is created by another Rake task called build
, but since the ʻinstall task depends on the
buildtask, just
rake install` will do both.
In order to do the above, Git must be installed and Rust (version 1.40.0 or higher) must be installed [^ rust1_40].
[^ rust1_40]: There is absolutely no reason to set the version of Rust to 1.40.0 or higher. There should be no problem with a lower version, but I don't know what happened with which version, so I tried to make it a little older than the latest version (1.46.0 at the moment). The lower limit of the version is set around the 9th line of ʻext / Rakefile`.
If the installation fails, I think it's about building the Rust code or moving the build product.
Rust is compiled at the time of installation, but it may take several tens of seconds in some cases because it also downloads dependent crates from the net. Please wait patiently even if there is no response.
For Linux, when installing Ruby system-wide and installing gems
sudo gem install hoge
I think you often have to add sudo
like this.
In this case, cargo must also work with sudo. Rust
require "rust_gem_sample01"
n = 10000
bin = Hash.new(0)
n.times do
bin[RustGemSample01.rand_norm(2.5).round] += 1
end
bin.keys.minmax.then{_1.._2}.each do |b|
puts "%4d %s" % [b, "*" * (bin[b].fdiv(n) * 60 * 4).round]
end
This draws a histogram (frequency distribution map) of a normal distribution random number with a mean of 0 and a standard deviation of 2.5 generated 10000 times and rounded to an integer. Generally, something like this is output.
-11
-10
-9
-8
-7 *
-6 **
-5 *****
-4 ***********
-3 ******************
-2 ******************************
-1 **********************************
0 ************************************
1 **********************************
2 *****************************
3 ******************
4 ***********
5 ******
6 **
7 *
8
9
10
Yeah, it looks like that.
If the program doesn't work and you get an error, please let me know.
.
├── bin
│ ├── console
│ └── setup
├── Cargo.toml
├── CHANGELOG.md
├── ext
│ └── Rakefile
├── Gemfile
├── lib
│ ├── rust_gem_sample01
│ │ └── version.rb
│ └── rust_gem_sample01.rb
├── LICENSE
├── pkg
├── Rakefile
├── README.md
├── rust_gem_sample01.gemspec
├── src
│ └── lib.rs
└── test
├── rust_gem_sample01_test.rb
└── test_helper.rb
By the way, this figure was made with the command line tool exa made by Rust.
As you can see, the files for gems and the files for the Rust package are mixed in the same hierarchy.
To briefly explain, the files around bin
, lib
, pkg
, Rakefile
, rust_gem_sample01.gemspec
, test
are gem.
Cargo.toml
, src
are Rust package files. If you build Rust, you can also create a target
directory.
This is fine because the file name and directory name are not covered, but I'm not sure if this is a good style. Rutie's sample had this kind of structure (I think). At least at first glance, I don't really think that it can be distinguished immediately. You might want to push the Rust relationship into one directory [^ files].
[^ files]: If you're building a gem from scratch, it might be easier to work with. First you can do bundle gem hoge
to create a gem project, then go inside and do cargo new fuga --lib
to create a Rust project.
The Rust code is a package with only the library crate rand_distr_for_ruby
.
Dependent crate
Cargo.toml
[dependencies]
rand = "0.7.3"
rand_distr = "0.3.0"
Only.
rand is familiar. rand_distr is a rand
. It seems that the function that generates the random number of is independent (I don't know).
This is the only code body:
src/lib.rs
use rand_distr::{Normal, Distribution};
#[no_mangle]
pub extern fn rand_norm(variance: f64) -> f64 {
let normal = Normal::new(0.0, variance).unwrap();
normal.sample(&mut rand::thread_rng())
}
It seems useless to generate rand_distr :: Normal
every time it is called.
Well, it's not for practical purposes.
If you want to generate a large number of random numbers with the same distribution, you want to do this only once. It's easy to keep the generated rand_distr :: Normal
all the time with Rutie (see [(5)](see https://qiita.com/scivola/items/2ab41a03beb00f6ce3d2)), but only with FFI. I'm not sure how to do it.
By the way, during development, in this state
cargo check
But
cargo build --release
I can do it. The advantage of this directory structure is that you don't have to go back and forth between hierarchies by putting Rust files in the root directory of your project.
This is the core code on the Ruby side.
rust_gem_sample01/lib/rust_gem_sample01.rb
require "ffi"
require "rust_gem_sample01/version"
module RustGemSample01
extend FFI::Library
lib_name = "rand_distr_for_ruby"
file_name =
case RbConfig::CONFIG["host_os"].downcase
when /darwin/ then "lib#{lib_name}.dylib"
when /mingw|mswin/ then "#{lib_name}.dll"
when /cygwin/ then "cyg#{lib_name}.dll"
else "lib#{lib_name}.so"
end
ffi_lib File.expand_path(file_name, __dir__)
attach_function :rand_norm, [:double], :double
end
I'm using a gem called ffi to assign Rust functions to Ruby methods. FFI (Foreign Function Interface), which makes it easy to handle a mechanism for exchanging between different languages.
Most of the code in this module definition is devoted to determining the arguments to pass to the ffi_lib
method.
Pass the path of the library file to be read to this method.
The library file compiled with Rust has a slightly different file name depending on the OS, so it is complicated like this.
I'm not sure if this process really fits, but the Rutie code [rutie-gem / lib / rutie.rb](https://github.com/danielpclark/rutie-gem/blob/ffaba4689f351a0acaf84067a6fca5c3b65bc9aa/lib/rutie. I referred to rb).
attach_function :rand_norm, [:double], :double
The part of is to generate Ruby methods in the module based on the loaded library.
The first argument is the name of the library function. This is also the name of the Ruby method.
The second argument is the type of the function argument. Since there can be multiple arguments, give them as an array. : double
is the name of the type defined by the FFI specification and represents a double precision floating point number. I don't know much about it, but it's probably the C language double
. Ruby's Float supports this.
The third argument is the return type of the function.
That's all it takes to use the functions of the library created by Rust on the Ruby side through FFI. Easy.
gemspec
Here is the place that needs special explanation about gemspec.
rust_gem_sample01/rust_gem_sample01.gemspec
#Excerpt
Gem::Specification.new do |spec|
#Omission
spec.add_dependency "ffi", "~> 1.13.1"
spec.extensions = %w[ext/Rakefile]
end
Since we are using the ffi gem, it is natural to add it to the runtime dependency (ʻadd_dependency`), but the important thing is the next
spec.extensions = %w[ext/Rakefile]
By the way. What is this?
This is the path to the Rake file that describes what to do when installing the gem. In the case of C extensions, the role is usually taken care of by a file called ʻextconf.rb`. For sqlite3 gem, this is sqlite3-ruby / ext / sqlite3 / extconf.rb. I'm not sure at all, but it seems that the C extension compiles C and so on according to this file.
However, it seems that ʻextconf.rb` is premised on C, and I'm not sure how to write it in the case of Rust. Or rather, I felt that it couldn't be used with Rust in the first place. When I searched for other means, I found that there was also a way to use the Rake task. This will be described in detail in the next section.
Rakefile
It seems that what you wrote as a default
task in the Rake file specified asspec.extensions =
in gemspec will be executed at installation time.
rust_gem_sample01/ext/Rakefile
#What to do when installing gem
#Describe as default task
task :default do
#Is Rust installed?
#Determine if the cargo command works
begin
cargo_v = `cargo -V`
rescue Errno::ENOENT
raise "Cargo not found. Install it."
end
#Is the Rust version (matching the Cargo version) above a certain level?
cargo_version = cargo_v.match(/\Acargo (\d+)\.(\d+)\.(\d+) /)[1..3].map(&:to_i)
if (cargo_version <=> [1, 40, 0]).negative?
raise "Too old Cargo (ver. #{cargo_v}). Update it."
end
#Build Rust
system "cargo build --release", chdir: __dir__ + "/.."
#Product file name
#Depends on OS
lib_name = "rand_distr_for_ruby"
file_name =
case RbConfig::CONFIG['host_os'].downcase
when /darwin/ then "lib#{lib_name}.dylib"
when /mingw|mswin/ then "#{lib_name}.dll"
when /cygwin/ then "cyg#{lib_name}.dll"
else "lib#{lib_name}.so"
end
#Product lib/Move directly below
FileUtils.mv __dir__ + "/../target/release/#{file_name}", __dir__ + "/../lib/"
FileUtils.rmtree __dir__ + "/../target/"
end
I wrote a comment in the code, but what I'm doing is basically
And just this.
I'm curious that the place to get the filename of the Rust code build product is lib / rust_gem_sample01.rb
and not DRY.
Well, it may be okay to move all the files directly under target / release /
.
By the way, if you build with Rust, you can create various files in the process of downloading and compiling the necessary libraries. In the case of this gem, a file of about 18 MB is generated at build time. All I want is one file (about 300 KB this time), and everything else is unnecessary.
So the last line of the task
FileUtils.rmtree __dir__ + "/../target/"
Is being cleaned by.
For C extensions, there is debate about whether gems should include compiled files for distribution.
reference:
There are pros and cons, but what about Rust? Perhaps you usually choose to precompile. That's because many Ruby users probably don't have a Rust compilation environment.
So, in this article, I dared to try a method that many people wouldn't do.