Slide 1

Slide 1 text

How to make a gem with Rust 鹿児島Ruby会議01 2019/11/30(Sat) 1

Slide 2

Slide 2 text

自己紹介 名前: 神速 会社: メドピア株式会社 出身: 鹿児島 ! GitHub: @sinsoku (アイコン右上) Twitter: @sinsoku_listy (アイコン右下) Ruby/Rails歴: 約7年 ⭐ Rust歴: 約3ヶ月 # 2

Slide 3

Slide 3 text

Rubyは良い言語です 3

Slide 4

Slide 4 text

しかし、いくつか弱点があります • ! 実行速度が遅い • " 型がない • # 実行時エラーが起きやすい • $ 少し飽きてきた 4

Slide 5

Slide 5 text

そんなときRustに出会った1 1 引用: 王様達のヴァイキング 11巻 5

Slide 6

Slide 6 text

公式ページに記載されてる特徴2 2 https://www.rust-lang.org/ 6

Slide 7

Slide 7 text

Rustの特徴 • ! 実行速度が速い • " 型、メモリ安全、スレッド安全 • # 分かりやすいエラーを備えたコンパイラ • ✨ ジェネリクス, パターンマッチ, Optional, ...etc モダンな機能をだいたい持っている 7

Slide 8

Slide 8 text

両方の良さを活かしたい • Rustを使えるエンジニアは少ない • Rustの求人はとても少ない • Ruby/Railsの良さは手放したくない • "Rubyを通して越境する" というテーマ メインはRails、一部にRustを使いたい 8

Slide 9

Slide 9 text

malept/thermite 9

Slide 10

Slide 10 text

テルミット法3 3 引用: https://ja.wikipedia.org/wiki/テルミット法 10

Slide 11

Slide 11 text

拡張ライブラリ? 11

Slide 12

Slide 12 text

拡張ライブラリとは • gem install のときにビルドするやつ • nokogiriやmysql2など • 処理の一部がC言語で書かれている • Rubyの内部メソッドを使える • メタプロ以上にヤバいことができる • SEGV... " 12

Slide 13

Slide 13 text

Exploring Internal Ruby Through C Extensions4 4 https://speakerdeck.com/yuryu/exploring-internal-ruby-through-c-extensions 13

Slide 14

Slide 14 text

bundle gem wasabi --ext Creating gem 'wasabi'... MIT License enabled in config Code of conduct enabled in config create wasabi/Gemfile create wasabi/lib/wasabi.rb create wasabi/lib/wasabi/version.rb create wasabi/wasabi.gemspec create wasabi/Rakefile create wasabi/README.md create wasabi/bin/console create wasabi/bin/setup create wasabi/.gitignore create wasabi/.travis.yml create wasabi/.rspec create wasabi/spec/spec_helper.rb create wasabi/spec/wasabi_spec.rb create wasabi/LICENSE.txt create wasabi/CODE_OF_CONDUCT.md create wasabi/ext/wasabi/extconf.rb create wasabi/ext/wasabi/wasabi.h create wasabi/ext/wasabi/wasabi.c Initializing git repo in /Users/sinsoku/.ghq/github.com/sinsoku/wasabi Gem 'wasabi' was successfully created. For more information on making a RubyGem visit https://bundler.io/guides/creating_gem.html 14

Slide 15

Slide 15 text

ext/wasabi/wasabi.c #include "wasabi.h" VALUE rb_mWasabi; void Init_wasabi(void) { rb_mWasabi = rb_define_module("Wasabi"); } 15

Slide 16

Slide 16 text

wasabi.gemspec Gem::Specification.new do |spec| spec.extensions = ["ext/wasabi/extconf.rb"] ext/wasabi/extconf.rb require "mkmf" create_makefile("wasabi/wasabi") 16

Slide 17

Slide 17 text

rake build mkdir -p tmp/x86_64-darwin18/wasabi/2.7.0 cd tmp/x86_64-darwin18/wasabi/2.7.0 /Users/sinsoku/.rbenv/versions/2.7.0-preview1/bin/ruby -I. ../../../../ext/wasabi/extconf.rb creating Makefile cd - cd tmp/x86_64-darwin18/wasabi/2.7.0 /usr/bin/make compiling ../../../../ext/wasabi/wasabi.c linking shared-object wasabi/wasabi.bundle cd - mkdir -p tmp/x86_64-darwin18/stage/lib/wasabi install -c tmp/x86_64-darwin18/wasabi/2.7.0/wasabi.bundle lib/wasabi/wasabi.bundle cp tmp/x86_64-darwin18/wasabi/2.7.0/wasabi.bundle tmp/x86_64-darwin18/stage/lib/wasabi/wasabi.bundle wasabi 0.1.0 built to pkg/wasabi-0.1.0.gem. lib/wasabi/wasabi.bundle が作られる。 17

Slide 18

Slide 18 text

拡張ライブラリの要約 • rake build すると lib/wasabi/wasabi.bundle ができる • reuqire "wasabi/wasabi"で拡張ライブラリ (*.so,*.o,*.dll など) を読み込める • 読み込んだときにC言語で定義した Init_xxx が呼ばれる 18

Slide 19

Slide 19 text

Thermite 19

Slide 20

Slide 20 text

Thermite v0.13.0には以下の問題がある • Macなのに*.soの拡張子が作られてしまう • 拡張ライブラリの出力先を変更できない • デフォルトだと lib/wasabi.bundle になる • masterでは変更できる 20

Slide 21

Slide 21 text

モンキーパッチを当てて直す # ext/build.rb require 'thermite/tasks' Thermite::Config.prepend(Module.new do def shared_library @shared_library ||= "#{library_name}.#{RbConfig::CONFIG['DLEXT'] || 'so'}" end def ruby_extension_dir @ruby_extension_dir ||= @options.fetch(:ruby_extension_dir, 'lib') end def ruby_extension_path ruby_path(ruby_extension_dir, shared_library) end end) root_path = File.expand_path('..', __dir__) Thermite::Tasks.new( cargo_project_path: root_path, ruby_project_path: root_path, ruby_extension_dir: 'lib/wasabi' ) 21

Slide 22

Slide 22 text

git diff -- Rakefile diff --git a/Rakefile b/Rakefile index e81a493..26e25a2 100644 --- a/Rakefile +++ b/Rakefile @@ -1,14 +1,15 @@ require "bundler/gem_tasks" require "rspec/core/rake_task" +require_relative 'ext/build' RSpec::Core::RakeTask.new(:spec) require "rake/extensiontask" -task :build => :compile +task :build => 'thermite:build' Rake::ExtensionTask.new("wasabi") do |ext| ext.lib_dir = "lib/wasabi" end -task :default => [:clobber, :compile, :spec] +task :default => [:clobber, 'thermite:build', :spec] 22

Slide 23

Slide 23 text

git diff -- ext/Rakefile diff --git a/ext/Rakefile b/ext/Rakefile new file mode 100644 index 0000000..2187642 --- /dev/null +++ b/ext/Rakefile @@ -0,0 +1,2 @@ +require_relative 'build' +task default: %w(thermite:build) 23

Slide 24

Slide 24 text

Rustでコードを書く 24

Slide 25

Slide 25 text

cargoで雛形を作る $ cargo init --lib Created library package $ git status --short M .gitignore ?? Cargo.toml ?? src/ 25

Slide 26

Slide 26 text

Cargo.tomlを更新 [lib] crate-type = ["cdylib"] [dependencies] ruru = { git = "https://github.com/d-unseductable/ruru" } ruby-sys = { git = "https://github.com/steveklabnik/ruby-sys" } [replace] "ruby-sys:0.3.0" = { git = "https://github.com/steveklabnik/ruby-sys" } 26

Slide 27

Slide 27 text

ruby-sys / ruru • ruby-sys • Rubyの低レベル関数のバインディング • rb_define_module など • ruru • ruby-sysより高レベルなAPIを提供する • 内部でruby-sysを使っている 27

Slide 28

Slide 28 text

Rustでコードを書く use ruby_sys::{class, fixnum, util}; use ruby_sys::types::{Value, SignedValue, CallbackPtr}; use ruru::util::str_to_cstring; use std::ptr; extern fn rb_sum(_mod: Value, a :Value, b: Value) -> Value { let a = unsafe { fixnum::rb_num2int(a) as i64 }; let b = unsafe { fixnum::rb_num2int(b) as i64 }; let sum = a + b; unsafe { fixnum::rb_int2inum(sum as SignedValue) } } extern fn rb_call_to_s(_mod: Value, obj: Value) -> Value { unsafe { let method_id = util::rb_intern(str_to_cstring("to_s").as_ptr()); util::rb_funcallv(obj, method_id, 0, ptr::null()) } } 28

Slide 29

Slide 29 text

Rustでコードを書く #[no_mangle] pub extern fn Init_wasabi() { unsafe { let rb_mod = class::rb_define_module(str_to_cstring("Wasabi").as_ptr()); class::rb_define_singleton_method(rb_mod, \ str_to_cstring("sum").as_ptr(), rb_sum as CallbackPtr, 2); class::rb_define_singleton_method(rb_mod, \ str_to_cstring("call_to_s").as_ptr(), rb_call_to_s as CallbackPtr, 1); } } 29

Slide 30

Slide 30 text

ビルドする $ rake build checking for cargo... yes /Users/sinsoku/.cargo/bin/cargo rustc --release --lib -- -C \ link-args=-L/Users/sinsoku/.rbenv/versions/2.7.0-preview1/lib \ -Wl,-undefined,dynamic_lookup -Wl,-multiply_defined,suppress Compiling libc v0.2.66 Compiling ruby-sys v0.3.0 (https://github.com/steveklabnik/ruby-sys#1b4c1412) Compiling lazy_static v0.2.11 Compiling ruru v0.9.3 (https://github.com/d-unseductable/ruru#587f1c40) Compiling wasabi v0.1.0 (/Users/sinsoku/.ghq/github.com/sinsoku/wasabi) Finished release [optimized] target(s) in 4.15s wasabi 0.1.0 built to pkg/wasabi-0.1.0.gem. 30

Slide 31

Slide 31 text

RSpecで動作確認 RSpec.describe Wasabi do describe '.sum' do it "1 + 2 = 3" do expect(Wasabi.sum(1, 2)).to eq 3 end end describe '.call_to_s' do context 'class with :to_s defined' do subject do klass = Class.new klass.define_method(:to_s) { 'foo' } Wasabi.call_to_s(klass.new) end it { is_expected.to eq 'foo' } end end end 31

Slide 32

Slide 32 text

Rustで書いたコードが動いた 32

Slide 33

Slide 33 text

Rustでgemを作るときの課題 • ! stableのgem/crateだと動かない • なぜかリリースされない • " 参考資料が少ない • C拡張に関するドキュメント • Ruby本体、ruru、ruby-sysのソースコード OSSはコードリーディングできるので便利 33

Slide 34

Slide 34 text

Run a Rails app that uses Rust extensions on Docker • https://github.com/sinsoku/rusty_rails • https://github.com/sinsoku/wasabi 話す時間が残っていれば... 34