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

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

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. 1
    mrubyで始める自作シェル
    高谷 雄貴 / GMO PEPABO inc.
    2022.10.15 Fukuoka mruby kaigi

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  10. # 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

    View Slide

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

    View Slide

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

    View Slide

  13. 13

    View Slide

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

    View Slide

  15. 15
    mrubyでシェルを作る

    View Slide

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

    View Slide

  17. 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でシェルを作る

    View Slide

  18. 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でシェルを作る

    View Slide

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

    View Slide

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

    View Slide

  21. 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を押す)
    (プログラムが終了する )

    View Slide

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

    View Slide

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

    View Slide

  24. 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でシェルを作る

    View Slide

  25. 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

    View Slide

  26. 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に追加する(👈が該当行)

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  30. 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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  35. 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とは?
    シェルに機能を追加する

    View Slide

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

    View Slide

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

    View Slide

  38. 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

    View Slide

  39. 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の作成

    View Slide

  40. 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) {
    }

    View Slide

  41. 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);
    }

    View Slide

  42. 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

    View Slide

  43. 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

    View Slide

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

    View Slide

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

    View Slide

  46. 46
    reddish-shellで発生した問題

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  51. 51
    まとめ

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide