RutieでRustのライブラリーをバインドしたRubyライブラリーを作る
題材:whatlang-rbというディレクトリーに、whatlangという名前のRubyGemを作りたい。 プロジェクトのディレクトリーを作る。
code:shell
bundle gem whatlang
これでディレクトリーとRuby用のボイラープレートができる。git initもされてる。 元々のRustのライブラリーとしてwhatlangがあるのでディレクトリー名を変える。 ディレクトリーをリネームする。
code:shell
mv whatlang whatlang-rb
bundle gem whalang-rbとやっちゃうと、whatlang-rbという名前のgemになっちゃうから最初はwhatlangでやりたい。
Rustでプロジェクトを初期化する時はディレクトリー名が読み取られるのでwhatlangじゃなくてwhatlang-rbにしたい。
という事情。
Rustプロジェクトの初期化をする。
code:shell
cd whatlang-rb
cargo init --lib
これでRust用のボイラープレートができる。.gitignoreにも追記される。
Rustプロジェクトを(実行ファイルではなく)ライブラリー用として宣言する
code:Cargo.toml.diff
diff --git a/Cargo.toml b/Cargo.toml
index 0c9e3a5..a72c9be 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -7,3 +7,6 @@ edition = "2018"
+
Cargo.tomlの依存にRutieを追加する。 code:shell
cargo add --features=no-link rutie
手で書き換えてもいいけど、cargo-editをインストールしてcargo addコマンドを使うのが楽だろう。
diffを見るとこうなる。
code:Cargo.toml.diff
diff --git a/Cargo.toml b/Cargo.toml
index a72c9be..072e50a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -7,6 +7,7 @@ edition = "2018"
+rutie = { version = "0.8.1", features = "no-link" } さて次はRubyだぞと思ってbundle installしようとすると、whatlang.gemspecにTODOが残っているとかで怒られるので、埋める。
code:whatlang.gemspec.diff
diff --git a/whatlang.gemspec b/whatlang.gemspec
index ebae745..fd9eb30 100644
--- a/whatlang.gemspec
+++ b/whatlang.gemspec
@@ -8,16 +8,14 @@ Gem::Specification.new do |spec|
- spec.summary = "TODO: Write a short summary, because RubyGems requires one."
- spec.description = "TODO: Write a longer description or delete this line."
- spec.homepage = "TODO: Put your gem's website or public repo URL here."
+ spec.summary = "Natural language detection."
+ spec.description = "Ruby bindings for whatlang, Natural language detection for Rust."
spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
-
- spec.metadata"changelog_uri" = "TODO: Put your gem's CHANGELOG.md URL here." # Specify which files should be added to the gem when it is released.
# The git ls-files -z loads the files in the RubyGem that have been added into git.
これでRubyの方の用意ができる。
code:shell
bundle config set --local vendor/path
bundle install
今作っているのは(アプリケーションではなく)ライブラリーなのでGemfile.lockはいらないので.gitignoreに追記。
code:.gitignore.diff
diff --git a/.gitignore b/.gitignore
index 0cd2cf1..da4e6c2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,7 +6,7 @@
/pkg/
/spec/reports/
/tmp/
-
+Gemfile.lock
# Added by cargo
Rakefileに、Rustのライブラリーをビルドするタスクを追加する。
code:Rakefile.diff
diff --git a/Rakefile b/Rakefile
index 8ca97e1..3675488 100644
--- a/Rakefile
+++ b/Rakefile
@@ -10,3 +10,14 @@ Rake::TestTask.new(:test) do |t|
end
task default: :test
+
+RUST_TARGET = "target/release/libwhatlang_rb.so"
+
+RUST_SRC.each do |path|
+ file path
+end
+
+file RUST_TARGET => RUST_SRC do
+ sh "cargo build --release"
+end
で、テストの依存にRustのバイナリーを加えてやれば、いつでも最新のRustバイナリーに対してテストできる。
code:Rakefile.diff
diff --git a/Rakefile b/Rakefile
index 3675488..b39e2b0 100644
--- a/Rakefile
+++ b/Rakefile
@@ -21,3 +21,4 @@ end
file RUST_TARGET => RUST_SRC do
sh "cargo build --release"
end
+task test: RUST_TARGET
さて、ここで、Cargo.toml内のバージョンとwhatlang.gemspec内のバージョンを合わせたい。whatlang.gemspecはRubyファイルなので、自由が利くからこの中で工夫してCargo.tomlとバージョンを合わせることにする。
まずはTOML用のライブラリーを入れる。
code:whalang.gemspec.diff
diff --git a/whatlang.gemspec b/whatlang.gemspec
index fd9eb30..dd01fbc 100644
--- a/whatlang.gemspec
+++ b/whatlang.gemspec
@@ -29,6 +29,8 @@ Gem::Specification.new do |spec|
# Uncomment to register a new dependency of your gem
# spec.add_dependency "example-gem", "~> 1.0"
+ spec.add_development_dependency "tomlrb"
+
# For more information and examples about making a new gem, checkout our
end
次にwhalang.gemspecを編集する。
code:whalang.gemspec.diff
diff --git a/whatlang.gemspec b/whatlang.gemspec
index dd01fbc..4bcd496 100644
--- a/whatlang.gemspec
+++ b/whatlang.gemspec
@@ -1,10 +1,11 @@
# frozen_string_literal: true
+require "tomlrb"
require_relative "lib/whatlang/version"
Gem::Specification.new do |spec|
spec.name = "whatlang"
- spec.version = Whatlang::VERSION
で、いらなくなったバージョン関係の記述を消す。
code:diff
diff --git a/lib/whatlang.rb b/lib/whatlang.rb
index e3427f8..868b8f3 100644
--- a/lib/whatlang.rb
+++ b/lib/whatlang.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require_relative "whatlang/version"
-
module Whatlang
class Error < StandardError; end
# Your code goes here...
diff --git a/whatlang.gemspec b/whatlang.gemspec
index 4bcd496..6134b0f 100644
--- a/whatlang.gemspec
+++ b/whatlang.gemspec
@@ -1,7 +1,6 @@
# frozen_string_literal: true
require "tomlrb"
-require_relative "lib/whatlang/version"
Gem::Specification.new do |spec|
spec.name = "whatlang"
code:shell
git rm lib/whatlang/version.rb
Rutie gemを追加する。
code:whatlang.gemspec.diff
diff --git a/whatlang.gemspec b/whatlang.gemspec
index 6134b0f..45e83ce 100644
--- a/whatlang.gemspec
+++ b/whatlang.gemspec
@@ -26,8 +26,7 @@ Gem::Specification.new do |spec|
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
spec.require_paths = "lib" - # Uncomment to register a new dependency of your gem
- # spec.add_dependency "example-gem", "~> 1.0"
+ spec.add_runtime_dependency "rutie"
spec.add_development_dependency "tomlrb"
RubyからRustのバイナリーを読み込む。
code:lib/whatlang.rb.diff
diff --git a/lib/whatlang.rb b/lib/whatlang.rb
index 868b8f3..44f2abb 100644
--- a/lib/whatlang.rb
+++ b/lib/whatlang.rb
@@ -1,6 +1,3 @@
-# frozen_string_literal: true
+require "rutie"
-module Whatlang
- class Error < StandardError; end
- # Your code goes here...
-end
+Rutie.new(:whatlang_rb).init "Init_whatlang", __dir__
この時点では、Rustで必要な処理を書いていないので読み込みエラーになる。(unknown symbol "Init_whatlang" (Fiddle::DLError))
Rustの方でWhatlangモジュールを定義してやる。
code:src/lib.rs.diff
diff --git a/src/lib.rs b/src/lib.rs
index 31e1bb2..f9207df 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,3 +1,11 @@
+use rutie::{Module, Object};
+
+pub extern "C" fn Init_whatlang() {
+ Module::new("Whatlang").define(|_itself| {});
+}
+
mod tests {
これでRustとRubyの間が繋がるようになった。
試しにテストを走らせてみると、適切に失敗する。
code:shell
bundle exec rake
cargo build --release
Compiling libc v0.2.81
Compiling rutie v0.8.1
Compiling lazy_static v1.4.0
Compiling whatlang-rb v0.1.0 (/home/kitaitimakoto/src/gitlab.com/KitaitiMakoto/whatlang-rb)
Finished release optimized target(s) in 4.64s Loaded suite /home/kitaitimakoto/src/gitlab.com/KitaitiMakoto/whatlang-rb/vendor/bundle/ruby/3.0.0/gems/rake-13.0.3/lib/rake/rake_test_loader
Started
F
===============================================================================
Failure: test: VERSION(WhatlangTest):
::Whatlang.const_defined?(:VERSION)
|
false
/home/kitaitimakoto/src/gitlab.com/KitaitiMakoto/whatlang-rb/test/whatlang_test.rb:7:in `block in <class:WhatlangTest>'
4:
5: class WhatlangTest < Test::Unit::TestCase
6: test "VERSION" do
=> 7: assert do
8: ::Whatlang.const_defined?(:VERSION)
9: end
10: end
===============================================================================
F
===============================================================================
Failure: test: something useful(WhatlangTest)
/home/kitaitimakoto/src/gitlab.com/KitaitiMakoto/whatlang-rb/test/whatlang_test.rb:13:in `block in <class:WhatlangTest>'
10: end
11:
12: test "something useful" do
=> 13: assert_equal("expected", "actual")
14: end
15: end
<"expected"> expected but was
<"actual">
diff:
? expected
? a ual
===============================================================================
Finished in 0.0234037 seconds.
-------------------------------------------------------------------------------
2 tests, 2 assertions, 2 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications
0% passed
-------------------------------------------------------------------------------
85.46 tests/s, 85.46 assertions/s
rake aborted!
Command failed with status (1)
/home/kitaitimakoto/src/gitlab.com/KitaitiMakoto/whatlang-rb/vendor/bundle/ruby/3.0.0/gems/rake-13.0.3/exe/rake:27:in `<top (required)>'
/home/kitaitimakoto/.gem/ruby/3.0.0/gems/bundler-2.2.3/lib/bundler/cli/exec.rb:63:in `load'
/home/kitaitimakoto/.gem/ruby/3.0.0/gems/bundler-2.2.3/lib/bundler/cli/exec.rb:63:in `kernel_load'
/home/kitaitimakoto/.gem/ruby/3.0.0/gems/bundler-2.2.3/lib/bundler/cli/exec.rb:28:in `run'
/home/kitaitimakoto/.gem/ruby/3.0.0/gems/bundler-2.2.3/lib/bundler/cli.rb:497:in `exec'
/home/kitaitimakoto/.gem/ruby/3.0.0/gems/bundler-2.2.3/lib/bundler/vendor/thor/lib/thor/command.rb:27:in `run'
/home/kitaitimakoto/.gem/ruby/3.0.0/gems/bundler-2.2.3/lib/bundler/vendor/thor/lib/thor/invocation.rb:127:in `invoke_command'
/home/kitaitimakoto/.gem/ruby/3.0.0/gems/bundler-2.2.3/lib/bundler/vendor/thor/lib/thor.rb:392:in `dispatch'
/home/kitaitimakoto/.gem/ruby/3.0.0/gems/bundler-2.2.3/lib/bundler/cli.rb:30:in `dispatch'
/home/kitaitimakoto/.gem/ruby/3.0.0/gems/bundler-2.2.3/lib/bundler/vendor/thor/lib/thor/base.rb:485:in `start'
/home/kitaitimakoto/.gem/ruby/3.0.0/gems/bundler-2.2.3/lib/bundler/cli.rb:24:in `start'
/home/kitaitimakoto/.gem/ruby/3.0.0/gems/bundler-2.2.3/exe/bundle:49:in `block in <top (required)>'
/home/kitaitimakoto/.gem/ruby/3.0.0/gems/bundler-2.2.3/lib/bundler/friendly_errors.rb:130:in `with_friendly_errors'
/home/kitaitimakoto/.gem/ruby/3.0.0/gems/bundler-2.2.3/exe/bundle:37:in `<top (required)>'
/home/kitaitimakoto/.gem/ruby/2.7.0/bin/bundle:23:in `load'
/home/kitaitimakoto/.gem/ruby/2.7.0/bin/bundle:23:in `<main>'
Tasks: TOP => default => test
(See full trace by running task with --trace)
自動でcargo buildが走っていること、Whatlangというモジュールその物はRubyの中で読み取れていることが分かる。
インストール時にRustバイナリーのビルドが走るようにする。
ext/Rakefileというファイルを作って、そこにビルド用のタスクを作る。タスク名はdefault。
code:ext/Rakefile
task :default do
sh "cargo build --release"
end
インストールする側にRustのビルド環境があることを前提にしている。
C拡張をインストールする時にはCのビルド環境があることを前提にしているのと一緒だよね?
あとはRutieのドキュメントを見ながら、RustからWhatlangの関数とかをRuby側に出してやったり、Ruby側で使い易いようにユーティリティコードを書いていけばよい。