Slide 1

Slide 1 text

1 mrubyで始める自作シェル 高谷 雄貴 / GMO PEPABO inc. 2022.10.15 Fukuoka mruby kaigi

Slide 2

Slide 2 text

GMOペパボ株式会社 プリンシパルエンジニア 2014年 中途入社 2 自己紹介 高谷 雄貴 Yuki Koya いわゆるインフラエンジニア。プライベートクラウドを はじめ各サービスの基盤や社内システムの面倒を見 ています。 ● Twitter : @buty4649 ● GitHub: buty4649

Slide 3

Slide 3 text

3 GMOペパボについて pepabo.com より

Slide 4

Slide 4 text

今日は仕事の話はしません! 趣味で作っているシェルの話をします 4

Slide 5

Slide 5 text

5 1. 自作シェルについて 2. mrubyでシェルを作る 3. シェルに機能を追加する 4. reddish-shellで発生した問題 5. まとめ アジェンダ

Slide 6

Slide 6 text

6 趣味で自作シェルを開発しています。 主な特徴として ● bashに似た構文 ● Rubyが使える ○ Rubyの構文を解釈 ○ Rubyスクリプトの実行 開発中なのでバグや機能不足があります。。 https://github.com/buty4649/reddish-shell reddish-shellという自作シェルを開発中 自作シェルについて

Slide 7

Slide 7 text

7 ● ある時Pythonが使えるシェルがあるということを知った ○ https://github.com/xonsh/xonsh ● PythonがあるならRubyのシェルがあってもいいのでは? ● シェルを作ろう! なぜreddish-shellを作ろうと思ったか? 自作シェルについて

Slide 8

Slide 8 text

8 1. bashに似た構文でRubyっぽくシェルスクリプトが書けるようにしたい 2. Rubyスクリプトも実行できるようにしたい 3. バイナリを配置するだけで実行できるようにしたい シェルを作るにあたり大まかな仕様を考える 自作シェルについて

Slide 9

Slide 9 text

9 ● 私がbashに慣れている/好き ● bashの構文が使えると世の中のシェルスクリプトを使い回せる ● Rubyっぽくシェルスクリプトが書けるととても楽しそう😆 ○ 実装が複雑になりそうだが実は bashとRubyの構文は似ていた(あとから気がついた ) 1. bashに似た構文でRubyっぽくシェルスクリプトが書けるようにしたい 自作シェルについて

Slide 10

Slide 10 text

# if文 if [ "$foo" = "bar" ]; then echo $foo fi # while 文 while [ $foo -le 10 ]; do echo $foo foo=`expr $foo + 1` done # for文 for foo in a b c; do echo $foo done 10 自作シェルについて bashとRubyは構文が似ている # if文 if foo == "bar" then puts foo end # while文 while foo <= 10 do puts foo foo += 1 end # for文 for foo in %w[a b c] do puts foo end bash Ruby if ~ then ~ fi/end while ~ do ~ done/end for ~ do ~ done/end

Slide 11

Slide 11 text

● Rubyスクリプトも実行できるとシェルスクリプトを書かなくてよくなる ○ 小さいバッチファイルも Rubyで書けるようになる ○ 書き捨てのシェルスクリプトはメンテナンスが大変 … ● 実行イメージ $ reddish-shell shell-script.sh hello world $ reddish-shell ruby-script.rb hello world 11 自作シェルについて 2. Rubyスクリプトも実行できるようにしたい

Slide 12

Slide 12 text

● 実行ファイルを置くだけで試せるのでお手軽 ● あわよくば既存のシェルの置き換えを目論んでいる…! ○ ログインシェルを私の自作シェルに置き換えるという野望! ● どうやってバイナリファイルを作成するか? ○ CRubyにバイナリファイルを生成する機能はない ■ ruby-packerを使う?(試したことがないのでわからない) ○ C言語 / Golang / Rust …etc 12 自作シェルについて 3.バイナリを配置するだけで実行できるようにしたい

Slide 13

Slide 13 text

13

Slide 14

Slide 14 text

● GolangやRustは学習コストの高さやRubyを組み込むのが大変そうなイメージがあった ○ 私がngx_mrubyの利用を通じてmrubyに触れていたのも大きい ● mruby APIが用意されているためC言語を使うのが良い気がするが・・・ ○ メモリ管理などを適切に行うのが大変 … ➢ mrubyならRubyを使いつつ足りない機能があれば拡張することができる ○ 基本的なロジックはRubyで書きmrubyにない機能はC言語で書くことにした 14 自作シェルについて mrubyを使うことにした

Slide 15

Slide 15 text

15 mrubyでシェルを作る

Slide 16

Slide 16 text

16 mrubyでシェルを作る ● ものすごく単純化してしまえば入力を受け取ってコマンドを実行するだけ ○ コマンド実行以外にも機能はあるがまずはここを目指す ● コマンドの実行にはexecシステムコールを使う ○ システムコールとはカーネルの特権機能を呼び出すこと ○ システムコールを使ったプログラミングをシステムプログラミングという そもそもシェルってどうやって作るの?

Slide 17

Slide 17 text

17 ● https://github.com/hone/mruby-cli ○ mrubyでCLIツールを作るときに便利なのだがあんまりメンテされていない … ○ mruby3.1.0対応版をforkして作った ■ https://github.com/buty4649/mruby-cli ● https://github.com/Asmod4n/mruby-linenoise ○ antirez/linenoiseのmrubyバインディングでヒストリーや補完機能などが使える ● https://github.com/haconiwa/mruby-exec ○ execシステムコールを使えるようにする シェルを作るときに便利なmrbgems mrubyでシェルを作る

Slide 18

Slide 18 text

18 ● mruby-cliはmrubyでCLIツールを作るときのスケルトンを用意してくれる ● Dockerイメージを用意しているのでそれを使うと便利 ● カレントディレクトリにmyshellという名前で作成する例 $ docker run --rm -v $(pwd):/home/mruby/code \ > buty4649/mruby-cli:3.1.0-focal mruby-cli -s myshell create .gitignore create mrbgem.rake create build_config.rb (中略) buty4649/mruby-cliの使い方 mrubyでシェルを作る

Slide 19

Slide 19 text

19 ● 作成されたディレクトリでrake compileするとビルドできる ● 実行例 $ cd myshell $ rake compile (中略) $ ./mruby/bin/myshell Hello World 実行ファイルの作成 mrubyでシェルを作る

Slide 20

Slide 20 text

20 ● tools/myshell/myshell.c ○ main関数がありmrubyを初期化 ○ __main__メソッドを呼び出し ● mrblib/myshell.rb ○ __main__メソッドがある ○ ここにRubyのコードを書く ● その他にも生成されるが割愛 生成されるファイルについて mrubyでシェルを作る myshell ├── mrblib │ └── myshell.rb └── tools └── myshell └── myshell.c

Slide 21

Slide 21 text

21 ● linenoiseメソッド ○ readlineのようなプロンプト ○ 入力された内容を返却 ○ Ctrl-Dでnilが返却される ● ヒストリーや補完機能の使い方は READMEを参照のこと mruby-linenoiseの使い方 mrubyでシェルを作る while (line = linenoise("shell> ")) unless line.empty? puts "line: #{line}" Linenoise::History.add(line) end end # 実行例 shell> hello line: hello shell> (Ctrl+Dを押す) (プログラムが終了する )

Slide 22

Slide 22 text

22 ● Exec.execvメソッド ○ 第1引数に実行するファイルのフ ルパスを指定 ○ 第2引数以降にプロセスの引数を 指定 mruby-execの使い方 mrubyでシェルを作る Exec.execv("/usr/bin/uname", "-r") # 実行例 5.15.0-46-generic (unameコマンドが実行され終了する )

Slide 23

Slide 23 text

23 mrubyでシェルを作る ● Exec.execvの第1引数がフルパスだったのもこれが原因 ● シェルでフルパス指定はめんどくさい ● PATH環境変数を見てファイルを検索できるようにするとよい ○ mrubyで環境変数を扱う場合は mruby-envを使う execシステムコールは実行するファイルをフルパスで指定する必要がある

Slide 24

Slide 24 text

24 def search_command(command) # 絶対パス、相対パスの場合はスキップ return command if command.include?("/") paths = ENV["PATH"].split(":").map{|p| "#{p}/#{command}"} result = paths.find{|p| File.exists?(p)} # 本当はパーミッションも確認するべき return result || command end # 実行例 search_command("cat") #=> /usr/bin/cat search_command("/usr/bin/cat") #=> /usr/bin/cat コマンド検索の実装例 mrubyでシェルを作る

Slide 25

Slide 25 text

25 def exec_command(input) command, *args = input.split(" ") cmd = search_command(command) Exec.execv(cmd, *args) end def search_command(command) return command if command.include?("/") paths = ENV["PATH"].split(":") .map{|p| "#{p}/#{command}"} result = paths.find{|p| File.exists?(p)} return result || command end (右に続く) シェルの実装 mrubyでシェルを作る 今までのコードを組み合わせて mrblib/myshell.rbに追加する。 def __main__(argv) while (line = linenoise("shell> ")) unless line.empty?    exec_command(line) end end end

Slide 26

Slide 26 text

26 require_relative 'mrblib/myshell/version' spec = MRuby::Gem::Specification.new('myshell') do |spec| spec.bins = ['myshell'] spec.add_dependency 'mruby-print', :core => 'mruby-print' spec.add_dependency 'mruby-mtest', :mgem => 'mruby-mtest' spec.add_dependency 'mruby-exec', github: 'haconiwa/mruby-exec' 👈 spec.add_dependency 'mruby-linenoise' 👈 spec.add_dependency 'mruby-env' 👈 end (以下省略) 使用するライブラリを依存関係に追加 mrubyでシェルを作る mrbgems.rakeに追加する(👈が該当行)

Slide 27

Slide 27 text

27 $ rake compile $ ./mruby/bin/myshell shell> echo hello world!! hello world!! 🎉🎉🎉🎉🎉🎉🎉🎉 ビルドして実行してみる mrubyでシェルを作る

Slide 28

Slide 28 text

28 ● execシステムコールは実行したプロセスを置換してコマンドを実行する ● これではシェルとしては機能しない ➢ そこでforkシステムコールを使用する コマンドを実行するとシェルも終了してしまう!! mrubyでシェルを作る

Slide 29

Slide 29 text

29 ● 呼び出したプロセスのコピーを作成し新しいプロセスを生成する ○ 呼び出したプロセスを親プロセス、生成されたプロセスを子プロセスと呼ぶ ● 子プロセスでexecシステムコールを実行する ● 親プロセスは子プロセスの終了を待つ ○ waitpidシステムコールを使用する ○ waitpidを呼び出さないと子プロセスはゾンビになる ● mruby-processにそれぞれシステムコールが実装されている forkシステムコールとは mrubyでシェルを作る

Slide 30

Slide 30 text

30 mrblib/myshell.rbへの変更 mrubyでシェルを作る mrbgem.rakeへの変更 spec.add_dependency 'mruby-exec', github:... spec.add_dependency 'mruby-linenoise' spec.add_dependency 'mruby-env' spec.add_dependency 'mruby-process' 👈 end def __main__(argv) while (line = linenoise("shell> ")) unless line.empty? # forkして子プロセスを生成する 👈 pid = Process.fork do 👈 exec_command(line) 👈 end 👈 # 子プロセスの終了を待つ 👈 Process.waitpid(pid) 👈 end end end

Slide 31

Slide 31 text

31 $ rake compile $ ./mruby/bin/myshell shell> echo hello world!! hello world!! shell> echo yattaze!! yattaze!! shell> 😆😆😆😆😆😆😆😆😆 ビルドして実行してみる mrubyでシェルを作る

Slide 32

Slide 32 text

32 シェルに機能を追加する

Slide 33

Slide 33 text

33 シェルに機能を追加する ● コマンドの入力元や出力先を変更する機能をリダイレクトという ● 今回は出力先をファイルに変更する機能を実装する ○ command > file の形式を想定 ● execシステムコールを呼び出す前に標準出力をファイルに変更する リダイレクト機能を追加する

Slide 34

Slide 34 text

34 ● IO.sysopenを呼び出しても標準出力を変更することはできない ● そこでdup2システムコールを使う ● 出力先のファイルをIO.sysopenしたあとにdup2で標準出力にコピーする ● mruby-ioにはIO.dup2は存在しない ➢ mrbgemを作成し機能を追加する必要がある! 標準出力をファイルに変更する シェルに機能を追加する

Slide 35

Slide 35 text

35 ● mrubyに機能を追加する仕組み ○ ドキュメント https://github.com/mruby/mruby/blob/master/doc/guides/mrbgems.md ● RubyもしくはC言語で書く ○ Rustで書いている人もいるらしい … https://tech.buty4649.net/entry/2021/11/26/122505 ● APIがありC言語からmrubyを扱うことができる ○ APIリファレンス https://mruby.org/docs/api/headers/mruby.h.html mrbgemとは? シェルに機能を追加する

Slide 36

Slide 36 text

36 ● mruby-ioを拡張してIOクラスにdup2メソッドを追加する ○ IO.dup2は2つのIntergerな引数を取りdup2関数に渡す ○ dup2関数の戻り値をそのまま mruby側に返却する ● 名前をmruby-io-extとする ● mrbgemは別のリポジトリにもできるが今回は同じディレクトリに配置する 作成するmrbgem シェルに機能を追加する

Slide 37

Slide 37 text

37 ● 必須のファイルは以下の2つ ○ mrbgem.rake ○ io_ext.c ● それぞれの配置場所は右図を参照 mrbgemに必要なファイル シェルに機能を追加する myshell └── mrbgems └── mruby-io-ext ├── mrbgem.rake 👈 └── src └── io_ext.c 👈

Slide 38

Slide 38 text

38 シェルに機能を追加する ● mrbgemのメタ情報を記述する ● 今回はmruby-ioの依存も追加 ● 詳しくは公式ドキュメントを参照のこと mruby-io-ext/mrbgem.rakeの作成 MRuby::Gem::Specification.new('mruby-io-ext') do |spec| spec.license = 'MIT' spec.author = 'buty4649' spec.summary = 'Add IO.dup2' spec.add_dependency('mruby-io') end

Slide 39

Slide 39 text

39 シェルに機能を追加する ● C言語のコードには以下の関数を含める必要がある ○ mrb_YOURGEMNAME_gem_init → 初期化処理を書く ○ mrb_YOURGEMNAME_gem_final → 後処理を書く ● YOURGEMNAMEにはmrbgemの名前が入る ○ 例えば今回なら mrb_mruby_io_ext_gem_init となる ● この関数を含めないとビルド時に undefined referenceエラーが出る mruby-io-ext/src/io_ext.cの作成

Slide 40

Slide 40 text

40 ● mrb_class_getを使いIOクラスを取得 ● mrb_define_class_methodを使い IOクラスにdup2メソッドを追加 ○ mrb_io_dup2関数をコールバック関 数に指定 ○ MRB_ARGS_REQ(2)は引数が2つ あ ることを宣言している ● 後処理は不要なので空 mrb_mruby_io_ext_gem_init/mrb_mruby_io_ext_gem_finalの実装 シェルに機能を追加する #include void mrb_mruby_io_ext_gem_init(mrb_state* mrb) { struct RClass *io; io = mrb_class_get(mrb, "IO"); mrb_define_class_method(mrb, io, "dup2", mrb_io_dup2, MRB_ARGS_REQ(2)); } void mrb_mruby_io_ext_gem_final(mrb_state* mrb) { }

Slide 41

Slide 41 text

41 ● mrb_get_argsを使い引数を取得 ○ sscanf関数のような使い方 ○ 第2引数に引数の型を指定する ○ 詳しくは公式ドキュメントを参照 ● mrb_sys_failはSystemCallErrorを raiseする ● mrb_fixnum_valueはint型を mrb_valueに変換する mrb_io_dup2の実装 シェルに機能を追加する mrb_value mrb_io_dup2(mrb_state* mrb, mrb_value self) { mrb_int oldfd, newfd; int ret; mrb_get_args(mrb, "ii", &oldfd, &newfd); ret = dup2(oldfd, newfd); if (ret == -1) { mrb_sys_fail(mrb, "dup2"); } return mrb_fixnum_value(ret); }

Slide 42

Slide 42 text

42 シェルに機能を追加する ● filenameで指定されたファイルを 書 き込みモードで開く ● IO.dup2を使い標準出力へコピー ● 不要になったコピー元をクローズする ● command > filenameの形なら redirect_to_fileを呼び出す シェル側にリダイレクト機能を実装 def redirect_to_file(filename) oldfd = IO.sysopen(filename, "w") newfd = 1 # 標準出力 IO.dup2(oldfd, newfd) IO._sysclose(oldfd) end def exec_command(input) command, *args = input.split(" ") if args[-2] == ">" redirect_to_file(args.last) args.pop; args.pop end cmd = search_command(command) Exec.execv(cmd, *args) end

Slide 43

Slide 43 text

43 シェルに機能を追加する ● mrbgem.rakeに依存を追加 ● pathを指定することでローカルの mrbgemを参照できる ○ mrubyディレクトリが基準に なる ので注意 ○ mrbgem.rake基準ではない mruby-io-extを使用するように変更 spec.add_dependency 'mruby-exec', github:... spec.add_dependency 'mruby-linenoise' spec.add_dependency 'mruby-env' spec.add_dependency 'mruby-process' spec.add_dependency 'mruby-io-ext', 👈 path: "#{__dir__}/mrbgems/mruby-io-ext" 👈 end

Slide 44

Slide 44 text

44 $ rake compile $ ./mruby/bin/myshell shell> echo hello world!! > testfile shell> cat testfile hello world!! shell> (Ctrl-Dを押す) 🥳🥳🥳🥳🥳🥳🥳 ビルドして実行してみる シェルに機能を追加する

Slide 45

Slide 45 text

45 ● 入力のリダイレクト ● フォアグラウンドプロセスとプロセスグループ ● コマンドのパイプ ● パーサーの実装(GNU bison) ● 制御構文の追加 ● シグナルハンドラ ● などなど・・・ 時間の関係で紹介できなかったこと シェルに機能を追加する

Slide 46

Slide 46 text

46 reddish-shellで発生した問題

Slide 47

Slide 47 text

47 reddish-shellで発生した問題 ● シェルの中でRubyスクリプトを実行したいが止める方法がない ○ 実行中のmruby_runを停止する方法がない ○ SignalException#Interrupt相当の処理がmrubyにはない ○ mrbgemでシグナルハンドラを作って外から mrb_exec_raiseしたりしたがうまく行かなかった ● いい解決案が思いつかないので無限ループしたらシェルごと落ちるという仕様にした ○ Done is better than perfect. 1. 無限ループしたRubyスクリプトを止める方法がない

Slide 48

Slide 48 text

48 reddish-shellで発生した問題 ● 一部の処理をスレッドで処理しようと思いmruby-threadを使っていた ○ 具体的にはシグナルハンドラにしようとしていた ● mruby-threadは新しいmruby VMにオブジェクトをコピーする処理になっている ○ オブジェクトが多すぎて (?)エラーになってしまう ● sigactionシステムコールを使ったシグナル処理に変更し回避した 2. mruby-threadを使うとSIGSEGVで終了してしまう

Slide 49

Slide 49 text

49 reddish-shellで発生した問題 ● bashと比べると10倍程度遅い ○ 1000回ループするwhile文を使ったシェルスクリプトで計測 ○ bash → 1sec, reddish-shell → 10sec ● 私の最適化不足も原因と考えられるが… ● 最適化するならC言語を使う割合を増やしていくほうが楽 ○ ただそれをするとmrubyでCLIを作る意味が薄れていく気がする … 3. コマンドの実行速度が遅くなってしまった

Slide 50

Slide 50 text

50 ● 色々考えた結果Rustで書き直している ○ 一度mrubyで実装していたので再実装自体は難しくなかった ○ Rustの習得とライブラリなどの理解の方が大変だった・・・ ● Rubyエンジン部分は今もmrubyを使っている 現在Rustで再実装しています reddish-shellで発生した問題

Slide 51

Slide 51 text

51 まとめ

Slide 52

Slide 52 text

52 まとめ ● mrubyを使うとRubyを使いながらシステムプログラミングできる ○ C言語を使う部分を最低限にできる ● シェルは様々なシステムコールを駆使しているのでシステムプログラミングの勉強になる ● mrbgemを作ることで機能を増やすことができ拡張性がある ● mrubyのコードが巨大になってくると問題が発生する ○ mrubyが組み込み用途なので私のケースではミスマッチになってしまったかも ○ それでもRubyで書き始められてシェル作成の理解が捗った部分もある mruby最高!

Slide 53

Slide 53 text

53 ● Webで使えるmrubyシステムプログラミング入門 (近藤 宇智朗著) ● 入門mruby Cからmruby APIを使いこなす (Yamanekko 著) 参考書籍 まとめ

Slide 54

Slide 54 text

54 ありがとうございました