Upgrade to Pro — share decks privately, control downloads, hide ads and more …

mrubyで始める自作シェル / Handmade bash-like shell with ...

buty4649
October 15, 2022

mrubyで始める自作シェル / Handmade bash-like shell with mruby

Fukuoka mruby Kaigi 2022で発表した資料です。
https://mrubykaigi.hp.peraichi.com/2022JP

buty4649

October 15, 2022
Tweet

More Decks by buty4649

Other Decks in Programming

Transcript

  1. GMOペパボ株式会社 プリンシパルエンジニア 2014年 中途入社 2 自己紹介 高谷 雄貴 Yuki Koya いわゆるインフラエンジニア。プライベートクラウドを

    はじめ各サービスの基盤や社内システムの面倒を見 ています。 • Twitter : @buty4649 • GitHub: buty4649
  2. 6 趣味で自作シェルを開発しています。 主な特徴として • bashに似た構文 • Rubyが使える ◦ Rubyの構文を解釈 ◦

    Rubyスクリプトの実行 開発中なのでバグや機能不足があります。。 https://github.com/buty4649/reddish-shell reddish-shellという自作シェルを開発中 自作シェルについて
  3. # 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
  4. 13

  5. • GolangやRustは学習コストの高さやRubyを組み込むのが大変そうなイメージがあった ◦ 私がngx_mrubyの利用を通じてmrubyに触れていたのも大きい • mruby APIが用意されているためC言語を使うのが良い気がするが・・・ ◦ メモリ管理などを適切に行うのが大変 …

    ➢ mrubyならRubyを使いつつ足りない機能があれば拡張することができる ◦ 基本的なロジックはRubyで書きmrubyにない機能はC言語で書くことにした 14 自作シェルについて mrubyを使うことにした
  6. 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でシェルを作る
  7. 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でシェルを作る
  8. 19 • 作成されたディレクトリでrake compileするとビルドできる • 実行例 $ cd myshell $

    rake compile (中略) $ ./mruby/bin/myshell Hello World 実行ファイルの作成 mrubyでシェルを作る
  9. 20 • tools/myshell/myshell.c ◦ main関数がありmrubyを初期化 ◦ __main__メソッドを呼び出し • mrblib/myshell.rb ◦

    __main__メソッドがある ◦ ここにRubyのコードを書く • その他にも生成されるが割愛 生成されるファイルについて mrubyでシェルを作る myshell ├── mrblib │ └── myshell.rb └── tools └── myshell └── myshell.c
  10. 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を押す) (プログラムが終了する )
  11. 22 • Exec.execvメソッド ◦ 第1引数に実行するファイルのフ ルパスを指定 ◦ 第2引数以降にプロセスの引数を 指定 mruby-execの使い方

    mrubyでシェルを作る Exec.execv("/usr/bin/uname", "-r") # 実行例 5.15.0-46-generic (unameコマンドが実行され終了する )
  12. 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でシェルを作る
  13. 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
  14. 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に追加する(👈が該当行)
  15. 27 $ rake compile $ ./mruby/bin/myshell shell> echo hello world!!

    hello world!! 🎉🎉🎉🎉🎉🎉🎉🎉 ビルドして実行してみる mrubyでシェルを作る
  16. 29 • 呼び出したプロセスのコピーを作成し新しいプロセスを生成する ◦ 呼び出したプロセスを親プロセス、生成されたプロセスを子プロセスと呼ぶ • 子プロセスでexecシステムコールを実行する • 親プロセスは子プロセスの終了を待つ ◦

    waitpidシステムコールを使用する ◦ waitpidを呼び出さないと子プロセスはゾンビになる • mruby-processにそれぞれシステムコールが実装されている forkシステムコールとは mrubyでシェルを作る
  17. 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
  18. 31 $ rake compile $ ./mruby/bin/myshell shell> echo hello world!!

    hello world!! shell> echo yattaze!! yattaze!! shell> 😆😆😆😆😆😆😆😆😆 ビルドして実行してみる mrubyでシェルを作る
  19. 33 シェルに機能を追加する • コマンドの入力元や出力先を変更する機能をリダイレクトという • 今回は出力先をファイルに変更する機能を実装する ◦ command > file

    の形式を想定 • execシステムコールを呼び出す前に標準出力をファイルに変更する リダイレクト機能を追加する
  20. 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とは? シェルに機能を追加する
  21. 37 • 必須のファイルは以下の2つ ◦ mrbgem.rake ◦ io_ext.c • それぞれの配置場所は右図を参照 mrbgemに必要なファイル

    シェルに機能を追加する myshell └── mrbgems └── mruby-io-ext ├── mrbgem.rake 👈 └── src └── io_ext.c 👈
  22. 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の作成
  23. 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 <mruby.h> 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) { }
  24. 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); }
  25. 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
  26. 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
  27. 44 $ rake compile $ ./mruby/bin/myshell shell> echo hello world!!

    > testfile shell> cat testfile hello world!! shell> (Ctrl-Dを押す) 🥳🥳🥳🥳🥳🥳🥳 ビルドして実行してみる シェルに機能を追加する
  28. 45 • 入力のリダイレクト • フォアグラウンドプロセスとプロセスグループ • コマンドのパイプ • パーサーの実装(GNU bison)

    • 制御構文の追加 • シグナルハンドラ • などなど・・・ 時間の関係で紹介できなかったこと シェルに機能を追加する
  29. 47 reddish-shellで発生した問題 • シェルの中でRubyスクリプトを実行したいが止める方法がない ◦ 実行中のmruby_runを停止する方法がない ◦ SignalException#Interrupt相当の処理がmrubyにはない ◦ mrbgemでシグナルハンドラを作って外から

    mrb_exec_raiseしたりしたがうまく行かなかった • いい解決案が思いつかないので無限ループしたらシェルごと落ちるという仕様にした ◦ Done is better than perfect. 1. 無限ループしたRubyスクリプトを止める方法がない
  30. 48 reddish-shellで発生した問題 • 一部の処理をスレッドで処理しようと思いmruby-threadを使っていた ◦ 具体的にはシグナルハンドラにしようとしていた • mruby-threadは新しいmruby VMにオブジェクトをコピーする処理になっている ◦

    オブジェクトが多すぎて (?)エラーになってしまう • sigactionシステムコールを使ったシグナル処理に変更し回避した 2. mruby-threadを使うとSIGSEGVで終了してしまう
  31. 49 reddish-shellで発生した問題 • bashと比べると10倍程度遅い ◦ 1000回ループするwhile文を使ったシェルスクリプトで計測 ◦ bash → 1sec,

    reddish-shell → 10sec • 私の最適化不足も原因と考えられるが… • 最適化するならC言語を使う割合を増やしていくほうが楽 ◦ ただそれをするとmrubyでCLIを作る意味が薄れていく気がする … 3. コマンドの実行速度が遅くなってしまった
  32. 52 まとめ • mrubyを使うとRubyを使いながらシステムプログラミングできる ◦ C言語を使う部分を最低限にできる • シェルは様々なシステムコールを駆使しているのでシステムプログラミングの勉強になる • mrbgemを作ることで機能を増やすことができ拡張性がある

    • mrubyのコードが巨大になってくると問題が発生する ◦ mrubyが組み込み用途なので私のケースではミスマッチになってしまったかも ◦ それでもRubyで書き始められてシェル作成の理解が捗った部分もある mruby最高!