こんにちは、hachi8833です。「Rustで書かれたRuby」であるArtichoke RubyをRustでビルドしてみました。Gobyもそうですが、ついこういうのをやってみたくなってしまう自分😅。
- Playground: Artichoke Ruby Playground
- リポジトリ: artichoke/artichoke: 💎 Artichoke is a Ruby made with Rust
artichoke: チョウセンアザミ(キク科の多年草)
特徴
- Rustで書かれている
- Ruby 2.6.3(MRI)互換を目指す
- Playgroundサイトですぐ試せる
- WebAssemblyベースでビルドされている
設計と目標(Readmeより)
- WebAssemblyのビルドをサポート
- 信頼できない環境でのRuby組み込みや実行をサポート
- Rubyアプリをシングルバイナリ配布する
- 最新のRuby dependencyを実装
- VMでの実験的サポート
- 動的コード生成
- 先読みコンパイル
- パラレル
- GILの排除
- 洗練されたメモリ管理
- GC
環境
Macbook + Parallels Desktop上の環境
Linux 5.0.0-25-generic #26~18.04.1-Ubuntu SMP Thu Aug 1 13:51:02 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
Rust: 1.40.0
このLinux環境にはLinuxbrewをインストールしてありますが、自分の場合はRust周りをLinuxbrewベースでインストールすると微妙にうまくいかなかったので、Linuxbrewは使わずにaptベースでやってみました。
clangも必要なので、sudo apt install clang
でインストールしました。
インストールとビルド
Rust環境の構築
結局ここが一番メインになります。
参考: Rustのインストール -- 公式の日本語訳
現時点で最新の方法でインストールしますが、Rustのインストール方法はちょくちょく変わっているようなので、公式をチェックしてください。
- 以下を実行してrustupをインストールします。特にカスタマイズはしないので
1) Proceed with installation (default)
を選択します。
curl https://sh.rustup.rs -sSf | sh
rustupは公式のインストーラであり、rustコンパイラ(rustc
)やパッケージ管理(cargo
)も含めて必要なものがインストールされます。
- Artichokeのリポジトリからローカルの適当なディレクトリに
git clone
します。 -
artichokeのディレクトリに移動し、以下を実行してビルドします。
cargo run -p artichoke-frontend --bin airb
なお、自分が最初にビルドしたときはpkg-configがないと言われたので、sudo apt-get install pkg-config
でインストールしました。
以下のようにビルドされ、IRB風のREPLが起動します。
$ cargo run -p artichoke-frontend --bin airb
Compiling libc v0.2.66
Compiling cfg-if v0.1.10
Compiling log v0.4.8
Compiling proc-macro2 v1.0.7
Compiling unicode-xid v0.2.0
Compiling autocfg v0.1.7
Compiling lazy_static v1.4.0
Compiling syn v1.0.12
Compiling memchr v2.2.1
Compiling byteorder v1.3.2
Compiling bitflags v1.2.1
Compiling glob v0.3.0
Compiling version_check v0.1.5
Compiling semver-parser v0.7.0
Compiling regex-syntax v0.6.12
Compiling unicode-width v0.1.7
Compiling failure_derive v0.1.6
Compiling shlex v0.1.1
Compiling proc-macro2 v0.4.30
Compiling rustc-demangle v0.1.16
Compiling peeking_take_while v0.1.2
Compiling vec_map v0.8.1
Compiling quick-error v1.2.2
Compiling unicode-xid v0.1.0
Compiling strsim v0.8.0
Compiling getrandom v0.1.13
Compiling ansi_term v0.11.0
Compiling termcolor v1.0.5
Compiling scopeguard v1.0.0
Compiling bindgen v0.50.1
Compiling rayon-core v1.7.0
Compiling pkg-config v0.3.17
Compiling target-lexicon v0.10.0
Compiling bindgen v0.51.1
Compiling ppv-lite86 v0.2.6
Compiling either v1.5.3
Compiling same-file v1.0.5
Compiling unicode-segmentation v1.6.0
Compiling fs_extra v1.1.0
Compiling nix v0.14.1
Compiling void v1.0.2
Compiling once_cell v1.2.0
Compiling arrayvec v0.5.1
Compiling smallvec v1.1.0
Compiling utf8parse v0.1.1
Compiling downcast v0.10.0
Compiling thread_local v0.3.6
Compiling path-dedot v1.1.13
Compiling crossbeam-utils v0.7.0
Compiling crossbeam-epoch v0.8.0
Compiling num-traits v0.2.10
Compiling num-integer v0.1.41
Compiling nom v4.2.3
Compiling semver v0.9.0
Compiling clang-sys v0.28.1
Compiling textwrap v0.11.0
Compiling humantime v1.3.0
Compiling c2-chacha v0.2.3
Compiling walkdir v2.2.9
Compiling heck v0.3.1
Compiling rustc_version v0.2.3
Compiling artichoke-core v0.1.0 (/media/psf/deve/ruby/artichoke/artichoke-core)
Compiling num_cpus v1.11.1
Compiling jobserver v0.1.17
Compiling atty v0.2.13
Compiling time v0.1.42
Compiling dirs-sys v0.3.4
Compiling quote v1.0.2
Compiling aho-corasick v0.7.6
Compiling fxhash v0.2.1
Compiling rustc-hash v1.0.1
Compiling regex-automata v0.1.8
Compiling memoffset v0.5.3
Compiling quote v0.6.13
Compiling cc v1.0.48
Compiling clap v2.33.0
Compiling rand_core v0.5.1
Compiling dirs v2.0.2
Compiling crossbeam-queue v0.2.1
Compiling regex v1.3.1
Compiling bstr v0.2.8
Compiling cexpr v0.3.6
Compiling libloading v0.5.2
Compiling backtrace-sys v0.1.32
Compiling rand_pcg v0.2.1
Compiling rand_chacha v0.2.1
Compiling rustyline v5.0.6
Compiling synstructure v0.12.3
Compiling syn-mid v0.4.0
Compiling chrono v0.4.10
Compiling env_logger v0.6.2
Compiling rustversion v1.0.1
Compiling rand v0.7.2
Compiling artichoke-vfs v0.5.0-alpha (/media/psf/deve/ruby/artichoke/artichoke-vfs)
Compiling proc-macro-error v0.4.4
Compiling proc-macro-error-attr v0.4.3
Compiling crossbeam-deque v0.7.2
Compiling rayon v1.3.0
Compiling structopt-derive v0.4.0
Compiling structopt v0.3.7
Compiling backtrace v0.3.40
Compiling failure v0.1.6
Compiling which v2.0.1
Compiling artichoke-backend v0.1.0 (/media/psf/deve/ruby/artichoke/artichoke-backend)
Compiling onig_sys v69.2.0
Compiling artichoke-backend v0.1.0 (/media/psf/deve/ruby/artichoke/artichoke-backend)
Compiling onig v5.0.0
Compiling artichoke-frontend v0.1.0 (/media/psf/deve/ruby/artichoke/artichoke-frontend)
Finished dev [unoptimized + debuginfo] target(s) in 1m 31s
Running `target/debug/airb`
artichoke 0.1.0 (2020-01-10 revision 2283) [x86_64-linux]
[Rust 1.40.0 (rev 73528e3) on x86_64-unknown-linux-gnu]
>>>
ビルドされたバイナリ(airb
)はtarget/debug/の下に配置されます。
動かしてみた
上の記事を元に2.6のコードを少し動かしてみました。
$ ./target/debug/airb
artichoke 0.1.0 (2020-01-10 revision 2283) [x86_64-linux]
[Rust 1.40.0 (rev 73528e3) on x86_64-unknown-linux-gnu]
>>> def m(*varargs, **keywords)
puts "varargs: #{varargs}"
puts "keywords: #{keywords}"
end
=> :m
>>> m
varargs: []
keywords: {}
=> nil
>>> m("a" => 1, b: 1)
Backtrace:
(airb):6: keyword argument hash with non symbol keys (ArgumentError)
(airb):6
キーワード引数で非シンボルを渡した場合の警告(Ruby 2.7で出なくなったヤツですね)は出ますが、メッセージがいつもと違う...これはmrubyの警告です。
>>> (1..6).step(2).to_a
Backtrace:
(airb):7: undefined method 'step' (NoMethodError)
(airb):7
artichokeではstep
が未実装ですね。Range
にどんなメソッドがあるかを見てみました。
(1..2).methods.sort
=> [:!, :!=, :!~, :==, :===, :Array, :Hash, :Integer, :String, :__case_eqq, :__id__, :__send__, :__to_int, :__to_str, :__update_hash, :all?, :any?, :begin, :block_given?, :catch, :class, :clone, :collect, :collect_concat, :count, :cover?, :cycle, :define_singleton_method, :detect, :drop, :drop_while, :dup, :each, :each_cons, :each_slice, :each_with_index, :each_with_object, :end, :entries, :enum_for, :eql?, :equal?, :eval, :exclude_end?, :extend, :find, :find_all, :find_index, :first, :flat_map, :format, :freeze, :frozen?, :global_variables, :grep, :group_by, :hash, :include?, :initialize, :initialize_copy, :inject, :inspect, :instance_eval, :instance_of?, :instance_variable_defined?, :instance_variable_get, :instance_variable_set, :instance_variables, :is_a?, :iterator?, :itself, :kind_of?, :lambda, :last, :lazy, :load, :local_variables, :loop, :m, :map, :max, :max_by, :member?, :method, :methods, :min, :min_by, :minmax, :minmax_by, :nil?, :none?, :object_id, :one?, :partition, :print, :private_methods, :proc, :protected_methods, :public_methods, :puts, :raise, :rand, :reduce, :reject, :remove_instance_variable, :require, :require_relative, :respond_to?, :reverse_each, :select, :send, :singleton_class, :singleton_method, :singleton_methods, :size, :sort, :sort_by, :sprintf, :srand, :take, :take_while, :tap, :then, :throw, :to_a, :to_enum, :to_h, :to_s, :uniq, :warn, :yield_self, :zip]
なお、以下はRuby 2.7.0のirbで同じことをやってみたものです。
irb(main):001:0> (1..2).methods.sort
=> [:!, :!=, :!~, :%, :<=>, :==, :===, :=~, :__id__, :__send__, :all?, :any?, :begin, :bsearch, :chain, :chunk, :chunk_while, :class, :clone, :collect, :collect_concat, :count, :cover?, :cycle, :define_singleton_method, :detect, :display, :drop, :drop_while, :dup, :each, :each_cons, :each_entry, :each_slice, :each_with_index, :each_with_object, :end, :entries, :enum_for, :eql?, :equal?, :exclude_end?, :extend, :filter, :filter_map, :find, :find_all, :find_index, :first, :flat_map, :freeze, :frozen?, :grep, :grep_v, :group_by, :hash, :include?, :inject, :inspect, :instance_eval, :instance_exec, :instance_of?, :instance_variable_defined?, :instance_variable_get, :instance_variable_set, :instance_variables, :is_a?, :itself, :kind_of?, :last, :lazy, :map, :max, :max_by, :member?, :method, :methods, :min, :min_by, :minmax, :minmax_by, :nil?, :none?, :object_id, :one?, :partition, :private_methods, :protected_methods, :public_method, :public_methods, :public_send, :reduce, :reject, :remove_instance_variable, :respond_to?, :reverse_each, :select, :send, :singleton_class, :singleton_method, :singleton_methods, :size, :slice_after, :slice_before, :slice_when, :sort, :sort_by, :step, :sum, :taint, :tainted?, :take, :take_while, :tally, :tap, :then, :to_a, :to_enum, :to_h, :to_s, :to_set, :trust, :uniq, :untaint, :untrust, :untrusted?, :yield_self, :zip]
Artichoke Rubyに実装済みのメソッドは思ったより多いようですが、未実装のメソッドもあちこちにあるようです。issueもかなり立っているので、先は長そうです。
気づいた点
1. バイナリがかなり大きい
シングルバイナリ配布を目指しているとはいえ、REPLバイナリのサイズが50MBというのはかなり大きい気がします。ただし昨年9月頃に試したときは85MBもあったので、かなり改善されていますね。
-rwxrwxr-x 1 hachi8833 hachi8833 50M 1月 10 10:58 airb
ちなみにUbuntuのRuby 2.7.0はバイナリ単体で19MB、参考までにGobyは機能はまだまだ少ないながらREPLも含めて13MBでした。
-rwxr-xr-x 1 hachi8833 hachi8833 190K 1月 10 13:32 ruby
-rwxr-xr-x 1 hachi8833 staff 13M 12 12 22:18 goby
なぜこんなに大きいのかなと思って軽く掘ってみると、artichoke-backend/の下にRuby 2.6.3を丸ごと飲み込んでいることに気づきました😳(若干パッチは当たっていますが)。
2. mrubyも実装に使っているらしい
#212を見ると、mrubyにバインドする形でRustで書いたメソッドを追加していることもあるようです。
artichoke-backendのREADMEをざっと見た限りでは、mruby(mruby-sys
というmrubyとRustのバインディング)をベースに動かしているようです。
artichoke-coreはどうやら、mrubyに依存しない形でのインタプリタ作りをここで進めているようです。
mruby依存から脱却中?
mrubyとCRubyで端的に動作が異なるコードはないかと思って探すと、以下の記事で1 / 2
の結果が異なることを知りました。mrubyではFloat
に、CRubyではInteger
になるそうです。以下の記事が書かれた2013年頃のRubyにはBigNum
がありましたね。
昨年9月頃の時点でartichokeのairb
で動かしてみたときは以下の結果になりました(すみません、当時のバージョンがわかりません🙇)。
>>> 1/2
=> 0.5
>>> (1/2).class
=> Float
その時点でのartichokeではこのあたりがmrubyに依存していたようです。
しかし今回artichoke 0.1.0でやってみると、結果が変わり、mrubyのFloat
ともCRubyのInteger
とも違うFixnum
になっていました。
$ ./target/debug/airb
artichoke 0.1.0 (2020-01-10 revision 2283) [x86_64-linux]
[Rust 1.40.0 (rev 73528e3) on x86_64-unknown-linux-gnu]
>>> 1/2
=> 0
>>> (1/2).class
=> Fixnum
まだ途中経過だと思われますが、artichoke-rubyはものすごい勢いでメンテされているので、今後はmruby依存から脱却してCRubyに近づいていくのだろうと推測しています。
おまけ
shiikaは、純粋にRustで書かれたRubyライクな言語だそうで、LLVMを出力するそうです。まだ自分の環境でビルドできていません😅。