I want to understand Rice that connects Ruby and C ++, so I look at Ankane's project morph-ruby.

Notice This article was written by someone who doesn't know C ++ or Rice. If you want to know the exact information, please hit the original text!

What is Rice?

Rice is the most famous way to connect Ruby and C ++. Recently, Andrew Kane is actively using Rice to create bindings for C ++ libraries and C ++ extensions for Ruby. In particular, Torch.rb, the most famous library for running Deep Learning in Ruby, is written in C ++, and I thought I should touch Rice a little.

Rice Japanese Tutorial

There are few articles in Japanese about Rice, but the next page will be helpful.

Learn how to use Gem with Rice from Ankane's project

Let's take the example of Rice and observe how it is used in the project. First, let's take a look at gemspec.

Gemspec

require_relative "lib/morph/version"

Gem::Specification.new do |spec|
#Omission
  spec.files         = Dir["*.{md,txt}", "{ext,lib}/**/*"]
  spec.require_path  = "lib"
  spec.extensions    = ["ext/morph/extconf.rb"]

  spec.required_ruby_version = ">= 2.5"

  spec.add_dependency "rice", ">= 2.2"
#Omission
end

It is written like this. Next, let's take a look at the Ruby files under lib.

Ruby files

lib/morph-ruby.rb

# ext
require "morph/ext"

# modules
require "morph/version"

Line 5 is really just requrire" morph/ext " because it just calls the version constant. It seems that you are loading ext.so (shared library extensions vary by platform).

extconf.rb

Looking at ext/morph /, there are only two files. extconf.rb and ext.cpp.

extconf.rb will generate a Makefile. It's the file specified by spec.extensions. Here it seems that have_library is used to check if there are any dependent libraries. It seems to be the same as the mkmf library that writes C language extensions. It also seems that you can specify compile-time flags by using the global variable $ CXXFLAGS. C ++ 17 seems to be one of the C ++ standards.

extconf.rb


require "mkmf-rice"

abort "Missing stdc++" unless have_library("stdc++")
abort "Missing ntl" unless have_library("ntl")
abort "Missing helib" unless have_library("helib")
abort "Missing morph" unless have_library("morph")

$CXXFLAGS << " -std=c++17"

create_makefile("morph/ext")

Next, finally look at the ext.cpp of Honmaru.

ext.cpp

#include <morph/client.h>

#include <rice/Array.hpp>
#include <rice/Class.hpp>
#include <rice/Constructor.hpp>
#include <rice/Hash.hpp>
#include <rice/Module.hpp>

using namespace Rice;

extern "C"
void Init_ext() {
  Module rb_mMorph = define_module("Morph");

  define_class_under<morph::Client>(rb_mMorph, "Client")
    .define_constructor(Constructor<morph::Client>())
    .define_method("keygen", &morph::Client::keygen)
    .define_method("set", &morph::Client::set)
    .define_method("flushall", &morph::Client::flushall)
    .define_method("dbsize", &morph::Client::dbsize)
    .define_method("info", &morph::Client::info)
    .define_method(
      "get",
      *[](morph::Client&self,conststd::string&key) {
        auto value = self.get(key);
        // TODO fix in C++ library
        return value.empty() ? Nil : String(value.c_str());
      })
    .define_method(
      "keys",
      *[](morph::Client&self,conststd::string&pattern) {
        auto keys = self.keys(pattern);
        Array res;
        for (auto &k : keys) {
          // TODO fix in C++ library
          res.push(k.c_str());
        }
        return res;
      });
}

I can't read C ++, but there are things that can be conveyed just by looking at it. First, the morph header file is specified.

#include <morph/client.h>

Second, you're probably ready to use Ruby modules, classes, arrays, and hashes.

#include <rice/Array.hpp>
#include <rice/Class.hpp>
#include <rice/Constructor.hpp>
#include <rice/Hash.hpp>
#include <rice/Module.hpp>

I'm not sure what's next, but it's safe to assume that it's magic.

extern "C"
void Init_ext() {
}

And we defined a module called Morph,

Module rb_mMorph = define_module("Morph");

You probably define a constructor or specify various methods in a class called Client.

define_class_under<morph::Client>(rb_mMorph, "Client")
    .define_constructor(Constructor<morph::Client>())
    .define_method("keygen", &morph::Client::keygen)
    .define_method("set", &morph::Client::set)
    .define_method("flushall", &morph::Client::flushall)
    .define_method("dbsize", &morph::Client::dbsize)
    .define_method("info", &morph::Client::info)

For methods that take arguments or return values, it's a bit complicated.

This is probably the case when the return value is string.

    .define_method(
      "get",
      *[](morph::Client&self,conststd::string&key) {
        auto value = self.get(key);
        // TODO fix in C++ library
        return value.empty() ? Nil : String(value.c_str());
      })

This is probably an array of return values.

    .define_method(
      "keys",
      *[](morph::Client&self,conststd::string&pattern) {
        auto keys = self.keys(pattern);
        Array res;
        for (auto &k : keys) {
          // TODO fix in C++ library
          res.push(k.c_str());
        }
        return res;
      });

Looking at it this way, unlike the case of FFI, it seems that you have to write the data exchange by yourself to some extent in Rice. On the other hand, since Ruby modules and classes can be generated in C ++, this is quite different from FFI, so we need to switch heads.

Rakefile

Finally, let's take a look at the Rakefile. You can see that you can add rake/extensiontask. Although it is remove_ext, it may not work in environments other than macOS.

require "bundler/gem_tasks"
require "rake/testtask"
require "rake/extensiontask"

task default: :test
Rake::TestTask.new do |t|
  t.libs << "test"
  t.pattern = "test/**/*_test.rb"
end

Rake::ExtensionTask.new("morph") do |ext|
  ext.name = "ext"
  ext.lib_dir = "lib/morph"
end

task :remove_ext do
  path = "lib/morph/ext.bundle"
  File.unlink(path) if File.exist?(path)
end

Rake::Task["build"].enhance [:remove_ext]

Overall file structure

.
├── CHANGELOG.md
├── ext
│  └── morph
│     ├── ext.cpp
│     └── extconf.rb
├── Gemfile
├── lib
│  ├── morph
│  │  ├── ext.so
│  │  └── version.rb
│  └── morph-ruby.rb
├── LICENSE.txt
├── morph-ruby.gemspec
├── pkg
│  └── morph-ruby-0.1.0.gem
├── Rakefile
├── README.md
└── test
   ├── client_test.rb
   └── test_helper.rb

The file structure looks like this. It's a reliable and secure Ankane project, so I think it's a good idea to use this as a template.

I'm really sorry for the thin entry, just looking at other people's projects, copying and pasting them, and expressing their impressions. Thanks to Ankane for continuing to make great gems. I hope it will be helpful for you.

That's all for this article.

Recommended Posts

I want to understand Rice that connects Ruby and C ++, so I look at Ankane's project morph-ruby.
Webpack and webpacker I want to tell Ruby people right now
[Ruby] I want to extract only the value of the hash and only the key
I want to RSpec even at Jest!