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

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

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

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

## 作ったやつ

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


https://hatsu38.github.io/ruby-wasm/validator/

![スクリーンショット 2023-12-08 1.03.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/107345/41497afb-a37a-926e-b9a7-0e6e9d2aa3ae.png)

### 正規表現をRubyでやるやつ

https://hatsu38.github.io/ruby-wasm/regex/

![スクリーンショット 2023-12-08 0.41.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/107345/ba92e638-2f47-94b6-5de8-0ed503f07996.png)

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

https://hatsu38.github.io/ruby-wasm/executor/

![スクリーンショット 2023-12-08 1.15.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/107345/d25981c9-29c4-f9d8-b570-8af4f88aab28.png)

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

https://github.com/hatsu38/ruby-wasm

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

```html
<!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>
```

ぱっと見もそれほど難しくないコードと思います。
```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でやる

https://hatsu38.github.io/ruby-wasm/validator/

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

<details><summary>全体はこちら</summary><div>

```html
<!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>
```

</div>
</details>

![スクリーンショット 2023-12-08 1.03.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/107345/41497afb-a37a-926e-b9a7-0e6e9d2aa3ae.png)

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

```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でやるやつ

https://hatsu38.github.io/ruby-wasm/regex/

<details><summary>全体はこちら</summary><div>

```html
<!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>
```

</div>
</details>

![スクリーンショット 2023-12-08 0.41.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/107345/66fc67a3-58bb-a001-a36f-a704b288c2a6.png)


Rubyの部分を抜粋。


```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
```

正規表現のフォーム人力された内容を元に、ここでチェック実行させています。
```ruby
regex = Regexp.new(pattern, options)
if (match = regex.match(text))
```

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


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

https://hatsu38.github.io/ruby-wasm/executor/

<details><summary>全体はこちら</summary><div>

```html
<!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>
```

</div>
</details>

![スクリーンショット 2023-12-08 1.15.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/107345/d25981c9-29c4-f9d8-b570-8af4f88aab28.png)

Ruby部分の抜粋。

```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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/107345/d8f905c5-1280-47fd-86c0-0be714c9e5d7.png)


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

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

## おわりに

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