LoginSignup
10
1

RubyのWasmに一歩だけ入門してみる

Last updated at Posted at 2023-12-08

Rubyの実行結果が欲しいけど、サーバー用意したりAPI用意したりとかまではしたくない。。
というきっかけもあって、ずっと気になってたWasmを使ってみようかなということが最近ありました。

私と同じくWasmを触ったことないような人にとって、この記事で最初の一歩くらいの、入門の手助けになればいいなと思っています。

自分がWasmを触る中で、こんなことできるんだなというの体感しながら、(似たようなものですが)3つくらい作ってみたので、それを用いながら入門していけたらと思います。

作ったやつ

バリデーションをJSじゃなくてRubyでやるやつ

スクリーンショット 2023-12-08 1.03.07.png

正規表現をRubyでやるやつ

スクリーンショット 2023-12-08 0.41.57.png

Rubyのコードをブラウザで実行できるやつ

スクリーンショット 2023-12-08 1.15.28.png

コードはぜんぶここにありますので、みたい方はどうぞです。

ざっくり使ってるテンプレート

<!DOCTYPE html>
<html lang="ja">
  <head>
    <script src="https://cdn.tailwindcss.com"></script>
    <!-- Wasmを使用するためのスクリプトを読み込み -->
    <script src="https://cdn.jsdelivr.net/npm/@ruby/3.2-wasm-wasi@2.3.0/dist/browser.script.iife.js"></script>
    <script type="text/ruby">
        # ここにRubyのコードを記述していく
      require 'js' 
      puts "Hellow Wasam!"

      def document
        @document ||= JS.global[:document]
      end
    </script>
  </head>
  <body>
  </body>
</html>

ぱっと見もそれほど難しくないコードと思います。

<script type="text/ruby">
# ここにRubyのコードを記述していく
require 'js' 
puts "Hellow Wasam!"

def document
  @document ||= JS.global[:document]
end
</script>

上記の部分にRubyのコードを書いていく感じですね。するとRubyがブラウザで実行できます。
putsの結果はConsoleに出力されます。

require 'js' をすることで、Rubyのコードから、JavaScriptのグローバルオブジェクトなどにアクセスできます。

バリデーションをJSじゃなくてRubyでやる

先ほどのテンプレートを用いながら、「バリデーションをJSじゃなくてRubyでやるやつ」のコードを見ていきます。

全体はこちら
<!DOCTYPE html>
<html lang="ja">
<head>
  <title>Validate</title>
  <meta name="description" content="Rubyを使用してデータの検証を行うサンプルページです。">
  <meta charset="utf-8"/>

  <script src="https://cdn.tailwindcss.com"></script>
  <!-- Wasmを使用するためのスクリプトを読み込みます。 -->
  <script src="https://cdn.jsdelivr.net/npm/@ruby/3.2-wasm-wasi@2.3.0/dist/browser.script.iife.js"></script>
  <script type="text/ruby">
    # frozen_string_literal: true
  
    require 'js'
  
    class Validator
      def initialize(document)
        @document = document
        @phone_input = document.getElementById('phone')
        @email_input = document.getElementById('email')
        @password_input = document.getElementById('password')
        setup_event_listeners
      end
  
      def validate_phone(text)
        cond = text.match?(/\A0\d{10}\Z/)
        err = @document.getElementById('phone_error')
        err[:innerText] = cond ? '' : '電話番号が不正です'
      end
  
      def validate_email(text)
        email_regex = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
        cond = text.match?(email_regex)
        err = @document.getElementById('email_error')
        err[:innerText] = cond ? '' : 'Emailが不正です'
      end
  
      def validate_password(text)
        password_regex = /\A(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}\z/
        cond = text.match?(password_regex)
        err = @document.getElementById('password_error')
        err[:innerText] = cond ? '' : 'パスワードが不正です(最低8文字、大文字、小文字、数字を含む必要があります)'
      end
  
      private
  
      def setup_event_listeners
        [@phone_input, @email_input, @password_input].each do |input|
          input.addEventListener 'input' do |e|
            method = "validate_#{input[:id]}"
            send(method, e[:target][:value].to_s)
          end
        end
  
        button = @document.getElementById('validateButton')
        button.addEventListener 'click' do
          validate_phone(@phone_input[:value].to_s)
          validate_email(@email_input[:value].to_s)
          validate_password(@password_input[:value].to_s)
        end
      end
    end
  
    def document
      @document ||= JS.global[:document]
    end
  
    Validator.new(document)
  </script>  
</head>
<body class="bg-gray-800 p-5 text-white">
  <div class="max-w-md mx-auto bg-gray-700 shadow-lg rounded p-5">
    <h1 class="text-xl font-bold text-white my-4 text-center">Ruby WASM Validator</h1>
    <label for="phone" class="block bg-gray-700 text-sm font-bold my-2">電話番号:</label>
    <input type="text" inputmode="numeric" id="phone" class="shadow appearance-none border rounded w-full py-2 px-3 bg-gray-700 leading-tight focus:outline-none focus:shadow-outline">
    <div id="phone_error" class="text-red-500 text-sm mt-2"></div>
    
    <label for="email" class="block bg-gray-700 text-sm font-bold my-2">Email:</label>
    <input type="email" id="email" class="shadow appearance-none border rounded w-full py-2 px-3 bg-gray-700 leading-tight focus:outline-none focus:shadow-outline">
    <div id="email_error" class="text-red-500 text-sm mt-2"></div>

    <label for="password" class="block bg-gray-700 text-sm font-bold my-2">パスワード:</label>
    <input type="password" id="password" class="shadow appearance-none border rounded w-full py-2 px-3 bg-gray-700 leading-tight focus:outline-none focus:shadow-outline" placeholder="パスワード">
    <div id="password_error" class="text-red-500 text-sm mt-2"></div>
    <label class="flex items-center mt-3">
      <input type="checkbox" id="passwordToggle" class="form-checkbox h-5 w-5 text-gray-600"><span class="ml-2 bg-gray-700">パスワードを表示</span>
    </label>

    <button id="validateButton" class="mt-4 bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">バリデートを実行</button>
  </div>
  <script>
    const passwordInput = document.getElementById("password")
    const passwordToggle = document.getElementById("passwordToggle")
    passwordToggle.addEventListener("click", () => {
      if (passwordInput.type === "password") {
        passwordInput.type = "text"
        passwordToggle.innerText = "パスワードを隠す"
      } else {
        passwordInput.type = "password"
        passwordToggle.innerText = "パスワードを表示"
      }
    })
  </script>
</body>
</html>

スクリーンショット 2023-12-08 1.03.07.png

Rubyの部分を抜粋しました。

    require 'js'
  
    class Validator
      def initialize(document)
        @document = document
        @phone_input = document.getElementById('phone')
        @email_input = document.getElementById('email')
        @password_input = document.getElementById('password')
        setup_event_listeners
      end
  
      def validate_phone(text)
        cond = text.match?(/\A0\d{10}\Z/)
        err = @document.getElementById('phone_error')
        err[:innerText] = cond ? '' : '電話番号が不正です'
      end
  
      def validate_email(text)
        email_regex = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
        cond = text.match?(email_regex)
        err = @document.getElementById('email_error')
        err[:innerText] = cond ? '' : 'Emailが不正です'
      end
  
      def validate_password(text)
        password_regex = /\A(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}\z/
        cond = text.match?(password_regex)
        err = @document.getElementById('password_error')
        err[:innerText] = cond ? '' : 'パスワードが不正です(最低8文字、大文字、小文字、数字を含む必要があります)'
      end
  
      private
  
      def setup_event_listeners
        [@phone_input, @email_input, @password_input].each do |input|
          input.addEventListener 'input' do |e|
            method = "validate_#{input[:id]}"
            send(method, e[:target][:value].to_s)
          end
        end
  
        button = @document.getElementById('validateButton')
        button.addEventListener 'click' do
          validate_phone(@phone_input[:value].to_s)
          validate_email(@email_input[:value].to_s)
          validate_password(@password_input[:value].to_s)
        end
      end
    end
  
    def document
      @document ||= JS.global[:document]
    end
  
    Validator.new(document)

これを見ると、がっつりRubyのコードって感じがしますね。Classとかメソッドを書けています。
addEventListenerでフォームの変更を検知したら、validationをvalidateのメソッドを動かすようにしていますね。

正規表現をRubyでやるやつ

全体はこちら
<!DOCTYPE html>
<html lang="ja">
<head>
  <title>Ruby WASM Regex Tester</title>
  <meta name="description" content="Ruby正規表現を試すことができるサンプルページです。">
  <meta charset="utf-8"/>

  <script src="https://cdn.tailwindcss.com"></script>
  <script src="https://cdn.jsdelivr.net/npm/@ruby/3.2-wasm-wasi@2.3.0/dist/browser.script.iife.js"></script>
  <script type="text/ruby">
    # frozen_string_literal: true

    require 'js'

    def document
      @document ||= JS.global[:document]
    end

    def test_regex(pattern, text, options)
      regex = Regexp.new(pattern, options)
      matches = text.scan(regex)
    
      if matches.any?
        highlighted_text = text.gsub(regex) { |match| "<span class='bg-yellow-400 text-black px-1 font-bold'>#{match}</span>" }
        status_message = "<span class='text-green-500'>#{matches.size}個のマッチが見つかりました</span>"
        result = highlighted_text
      else
        status_message = "<span class='text-red-500'>マッチしませんでした。</span>"
        result = text
      end
    
      document.getElementById('statusMessage')[:innerHTML] = status_message
      document.getElementById('result')[:innerHTML] = result
    end

    button = document.getElementById('runButton')
    regex_input = document.getElementById('regex')
    test_input = document.getElementById('testString')
    options_input = document.getElementById('options')

    button.addEventListener 'click' do
      test_regex(regex_input[:value].to_s, test_input[:value].to_s, options_input[:value].to_s)
    end
  </script>
</head>
<body class="bg-gray-800 p-5 text-white">
  <div class="max-w-xl mx-auto bg-gray-700 border border-gray-600 shadow-lg rounded p-5">
    <h1 class="text-xl font-bold text-white my-4 text-center">Ruby WASM Regex Tester</h1>
    <div class="mb-4 flex items-center">
      <span class="leading-tight text-2xl mr-1">/</span>
      <input type="text" id="regex" class="w-full shadow appearance-none border border-gray-600 rounded py-2 px-3 bg-gray-800 leading-tight focus:outline-none focus:shadow-outline inline-block" value="https?:\/\/[\w]+\.[\w]+(?:\.[\w]+)+[\/\w._?%&=-]*">
      <span class="leading-tight text-2xl mx-1">/</span>
      <input type="text" id="options" class="w-16 shadow appearance-none border border-gray-600 rounded py-2 px-3 bg-gray-800 leading-tight focus:outline-none focus:shadow-outline inline-block" placeholder="imx">
    </div>

    <label for="testString" class="block text-sm font-bold mb-2">テスト文字列:</label>
    <textarea id="testString" class="shadow appearance-none border border-gray-600 rounded w-full py-2 px-3 bg-gray-800 leading-tight focus:outline-none focus:shadow-outline" rows="4">こちらはテスト文章です。URLの例: https://www.example.com</textarea>

    <button id="runButton" class="mt-4 bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">正規表現をテスト</button>

    <div class="text-sm mt-4 p-3">
      <span>実行結果:</span>
      <span id="statusMessage">-</span>
    </div>
    <div id="result" class="text-sm mt-4 p-3 border border-gray-600 rounded bg-gray-900"> </div>
  </div>
</body>
</html>

スクリーンショット 2023-12-08 0.41.57.png

Rubyの部分を抜粋。

    # frozen_string_literal: true

    require 'js'

    def document
      @document ||= JS.global[:document]
    end

    def test_regex(pattern, text, options)
      regex = Regexp.new(pattern, options)
      matches = text.scan(regex)
    
      if matches.any?
        highlighted_text = text.gsub(regex) { |match| "<span class='bg-yellow-400 text-black px-1 font-bold'>#{match}</span>" }
        status_message = "<span class='text-green-500'>#{matches.size}個のマッチが見つかりました</span>"
        result = highlighted_text
      else
        status_message = "<span class='text-red-500'>マッチしませんでした。</span>"
        result = text
      end
    
      document.getElementById('statusMessage')[:innerHTML] = status_message
      document.getElementById('result')[:innerHTML] = result
    end

    button = document.getElementById('runButton')
    regex_input = document.getElementById('regex')
    test_input = document.getElementById('testString')
    options_input = document.getElementById('options')

    button.addEventListener 'click' do
      test_regex(regex_input[:value].to_s, test_input[:value].to_s, options_input[:value].to_s)
    end

正規表現のフォーム人力された内容を元に、ここでチェック実行させています。

regex = Regexp.new(pattern, options)
if (match = regex.match(text))

Rubyの正規表現をRubyで実行できるのが便利です!

Rubyのコードをブラウザで実行できるやつ

全体はこちら
<!DOCTYPE html>
<html lang="ja">
  <head>
    <title>Ruby Code Executor</title>
    <meta name="description" content="このページでは、Rubyコードをwasmを用いてオンラインで実行し、結果を表示します。">
    <meta charset="utf-8"/>

    <script src="https://cdn.tailwindcss.com"></script>
    <script src="https://cdn.jsdelivr.net/npm/@ruby/3.2-wasm-wasi@2.3.0/dist/browser.script.iife.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.32.0/ace.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>

    <script type="text/ruby">
      # frozen_string_literal: true

      require 'js'
      
      def document
        @document ||= JS.global[:document]
      end
      
      def execute_ruby_code(code)
        result = eval(code)
        document.getElementById('result')[:innerText] = "結果: #{result}"
      rescue Exception => e
        document.getElementById('result')[:innerText] = "エラー: #{e.message}"
      end
      
      button = document.getElementById('executeButton')
      
      button.addEventListener 'click' do |_e|
        ruby_code = document.getElementById('ruby_code')[:value].to_s
        execute_ruby_code(ruby_code)
      end
    </script>
  </head>
  <body class="bg-gray-800 p-5 text-white">
    <div class="container mx-auto bg-gray-700 shadow-lg rounded p-5">
      <h1 class="text-xl font-bold text-white my-4 text-center">Ruby Code Executor</h1>
      <label for="ruby_code_editor" class="block text-gray-700 text-sm font-bold mb-2 text-white">Rubyコード:</label>
      <div id="ruby_code_editor" class="w-full h-96 rounded"></div>
      <textarea id="ruby_code" class="hidden">class Fibonacci
  def initialize
    @num1, @num2 = 0, 1
  end

  def next_number
    @num1, @num2 = @num2, @num1 + @num2
    @num1
  end
  
  def answer
    @num1
  end
end

fib = Fibonacci.new
10.times { puts fib.next_number }

fib.answer</textarea>
      <button id="executeButton" class="mt-4 bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">実行</button>
      <div id="result" class="text-green-500 text-sm mt-2"></div>
    </div>
    <script>
      const editor = ace.edit("ruby_code_editor");
      editor.setValue(`class Fibonacci
  def initialize
    @num1, @num2 = 0, 1
  end

  def next_number
    @num1, @num2 = @num2, @num1 + @num2
    @num1
  end
  
  def answer
    @num1
  end
end

fib = Fibonacci.new
10.times { puts fib.next_number }

fib.answer
`);
      editor.setTheme("ace/theme/monokai");
      editor.session.setMode("ace/mode/ruby");
      editor.session.setUseSoftTabs(true);
      editor.setFontSize(14);
      editor.resize()
      editor.getSession().setUseWrapMode(true);
      editor.getSession().setTabSize(2);
      editor.$blockScrolling = Infinity;
    
      editor.setOptions({
        enableBasicAutocompletion: true,
        enableSnippets: true,
        enableLiveAutocompletion: true
      });

      editor.on('change', (arg, activeEditor) => {
        const aceEditor = activeEditor;
        const newHeight = aceEditor.getSession().getScreenLength() *
          (aceEditor.renderer.lineHeight + aceEditor.renderer.scrollBar.getWidth());
        aceEditor.container.style.height = `${newHeight}px`;
        aceEditor.resize();
      });

      editor.getSession().on('change', function(){
        const code = editor.getValue();
        document.getElementById("ruby_code").value = code;
      });
    </script>
  </body>
</html>

スクリーンショット 2023-12-08 1.15.28.png

Ruby部分の抜粋。

# frozen_string_literal: true

require 'js'

def document
  @document ||= JS.global[:document]
end

def execute_ruby_code(code)
  result = eval(code)
  document.getElementById('result')[:innerText] = "結果: #{result}"
rescue Exception => e
  document.getElementById('result')[:innerText] = "エラー: #{e.message}"
end

button = document.getElementById('executeButton')

button.addEventListener 'click' do |_e|
  ruby_code = document.getElementById('ruby_code')[:value].to_s
  execute_ruby_code(ruby_code)
end

evalでTextareaに入力されたコードを実行していますね。
作った中でこれが一番色々できて面白いなと感じました。

例えば、こんな感じでFiberも動きました!すごい!

スクリーンショット 2023-12-08 2.46.51.png

セキュリティ上の注意
ユーザーの入力を直接 eval で実行する場合は特に注意が必要です。実際に使う際には、サニタイズなどをして実行するのが良いかと思います。

この記事に記載したサイトにも、イタズラはやめてね 🙏

おわりに

Wasmではもっと多くのことができますが、とりあえずの一歩目と言うことでこんな記事を書きました。同じく入門する人の役に立ったらなと思います。
もう少し慣れたら、Wasmを使って便利になる箇所とかで実運用で使ってみたいですね

10
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
1