WebAssemblyを使った開発をやってみた

## はじめに
初めましての人もそうでない人もこんにちは！

最近は寒すぎてこたつを導入するか毛布で耐えるかめっちゃ悩んでいます！
皆さんは、まだ寒さに耐えられているでしょうか？風邪をひかないようにきをつけてくださいね！

今回はWebAssemblyを使って文字列を変換するアプリを作成してみました！
少し前にTwitter(現X)で「かれし」=「かれぴっぴ」なら「ししおどし」=「ぴっぴぴっぴおどぴっぴ」みたいなツイートを見て大爆笑したんですよね！
確かこの人だったはず・・・

https://x.com/hiyoooooorin/status/836461638416908288

https://detail.chiebukuro.yahoo.co.jp/qa/question_detail/q1485252234

少々お下品なので多くは語りませんがこれも似たような例ですね

そこで今回は例のように文字と文字を変換した文章を変換するアプリを作成しました！

あと今回は初めての挑戦としてWebAssemblyを使った開発もしてみたのでぜひ最後までご覧ください！

## WebAssemblyとは
一番参考になりそうなのは以下の記事ではないでしょうか？

https://qiita.com/ShuntaShirai/items/3ac92412720789576f22

私なりに要約をしてみると
- ブラウザ技術の発展により複雑な処理が増加し、処理速度の問題が発生
- asm.jsが登場し実行速度が向上したがファイルサイズが増大してしまうなど様々な課題が存在
- そこで、コンピュータが直接理解できる形式で書かれたプログラムを、Webブラウザ上で実行するための新しい仕組みとして生まれたのがWebAssemblyという仕組み

要するに、めっちゃ処理速度が速くなる仕組みだよということです！
間違っていたらすみません💦

今回は高度な処理が必要な開発は行いませんが一連の流れを紹介できればと思っています！

## ディレクトリ構成
```
karepippi/
│
├── frontend/
│    ├── public/
│    │   ├── wams/
│    │   │   ├── main.wams
│    │   │   └── wams_exes.js
│    │   └── index.html
│    ├── src/
│    │   ├── App.tsx
│    │   ├── InputForm.tsx
│    │   ├── styles.css
│    │   └── ...
│    └── ...
└── wasm/
     ├── main.go
     └── go.mod
```

## 実装したい機能
- WebAssemblyの実装
- 文字を置き換える機能

## 作ってみよう！
**環境構築・フロントエンド**
```
mkdir karepippi
cd karepippi
npx create-react-app frontend --template typescrpit
cd frontend/src
touch InputForm.tsx
touch styles.css
```
**環境構築・バックエンド**
```
mkdir wasm
cd wasm
touch main.go
go mod init wasm
```
**バックエンド開発**
```go:wasm/ main.go
package main

import (
    "regexp"
    "strings"
    "syscall/js"
)

// ValidationResult は入力チェックの結果を保存する構造
type ValidationResult struct {
    IsValid bool
    Message string
}

// ReplacementResult は文字列の置換結果を保存する構造
type ReplacementResult struct {
    DisplayText string
    Error       string
}

// validateKana は入力された文字列がひらがな・カタカナのみかをチェックする関数
// 以下のルールで判定します：
// 1. 空文字の場合はOK
// 3. ひらがな・カタカナ以外の文字が含まれている場合はエラー
func validateKana(input string) ValidationResult {
    if input == "" {
        return ValidationResult{IsValid: true, Message: ""}
    }
    if len([]rune(input)) > 10 {
        return ValidationResult{IsValid: false, Message: "10文字以内で入力してください"}
    }
    // ひらがな・カタカナだけが含まれているかをチェックします
    pattern := regexp.MustCompile(`^[\p{Hiragana}\p{Katakana}]+$`)
    if !pattern.MatchString(input) {
        return ValidationResult{IsValid: false, Message: "ひらがなまたはカタカナのみ入力できます"}
    }
    return ValidationResult{IsValid: true, Message: ""}
}

// validateShortText は入力された文字列の長さが適切かをチェックする関数です
func validateShortText(input string) ValidationResult {
    if len([]rune(input)) > 30 {
        return ValidationResult{IsValid: false, Message: "30文字以内で入力してください"}
    }
    return ValidationResult{IsValid: true, Message: ""}
}

// performReplacement は文字列の置換を行う関数
// - searchText: 検索する文字列
// - replaceText: 置き換える文字列
// - originalText: 元の文字列
func performReplacement(searchText string, replaceText string, originalText string) ReplacementResult {
    // 検索文字列か元の文字列が空の場合は、置換せずに返す
    if searchText == "" || originalText == "" {
        return ReplacementResult{DisplayText: originalText, Error: ""}
    }
    
    // 入力内容のチェック
    kanaValidation := validateKana(searchText)
    if !kanaValidation.IsValid {
        return ReplacementResult{DisplayText: originalText, Error: kanaValidation.Message}
    }
    shortTextValidation := validateShortText(replaceText)
    if !shortTextValidation.IsValid {
        return ReplacementResult{DisplayText: originalText, Error: shortTextValidation.Message}
    }
    
    // 文字列の置換を実行
    result := strings.ReplaceAll(originalText, searchText, replaceText)
    return ReplacementResult{DisplayText: result, Error: ""}
}

// validateKanaWrapper はGo言語の関数をJavaScriptから呼び出せるようにする関数
// JavaScript側からの入力を受け取り、Go言語の関数を実行して結果を返す
func validateKanaWrapper(this js.Value, args []js.Value) interface{} {
    if len(args) != 1 {
        return map[string]interface{}{
            "isValid": false,
            "message": "Invalid arguments",
        }
    }
    input := args[0].String()
    result := validateKana(input)
    return map[string]interface{}{
        "isValid": result.IsValid,
        "message": result.Message,
    }
}

// replaceTextWrapper はGo言語の文字列置換機能をJavaScriptから呼び出せるようにする関数
// JavaScript側からの入力を受け取り、Go言語の関数を実行して結果を返す
func replaceTextWrapper(this js.Value, args []js.Value) interface{} {
    if len(args) != 3 {
        return map[string]interface{}{
            "displayText": "",
            "error":      "Invalid arguments",
        }
    }
    searchText := args[0].String()
    replaceText := args[1].String()
    originalText := args[2].String()
    result := performReplacement(searchText, replaceText, originalText)
    return map[string]interface{}{
        "displayText": result.DisplayText,
        "error":      result.Error,
    }
}

func main() {
    c := make(chan struct{}, 0)
    // validateKanaWasm: ひらがな・カタカナのチェック用
    // replaceTextWasm: 文字列置換用
    js.Global().Set("validateKanaWasm", js.FuncOf(validateKanaWrapper))
    js.Global().Set("replaceTextWasm", js.FuncOf(replaceTextWrapper))
    
    // プログラムを継続して実行させる
    <-c
}
```

**フロントエンド開発**
```tsx:src/InputForm.tsx
import React, { useState, useEffect } from 'react';
import './styles.css';

declare global {
  interface Window {
    validateKanaWasm: (input: string) => { isValid: boolean; message: string };
    replaceTextWasm: (search: string, replace: string, text: string) => { displayText: string; error: string };
    Go: any;
  }
}

const InputForm: React.FC = () => {
  const [kanaInput, setKanaInput] = useState('');
  const [shortText, setShortText] = useState('');
  const [longText, setLongText] = useState('');
  const [displayText, setDisplayText] = useState('');
  const [error, setError] = useState('');
  const [wasmLoaded, setWasmLoaded] = useState(false);

  // WebAssemblyの初期化
  useEffect(() => {
    const initWasm = async () => {
      try {
        const go = new window.Go();
        const result = await WebAssembly.instantiateStreaming(
          fetch('/wasm/main.wasm'),
          go.importObject
        );
        go.run(result.instance);
        setWasmLoaded(true);
      } catch (error) {
        console.error('Failed to load WASM:', error);
        setError('WebAssemblyの読み込みに失敗しました');
      }
    };

    initWasm();
  }, []);

  // 入力の処理
  const handleKanaInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const value = e.target.value;
    setKanaInput(value);

    if (wasmLoaded && window.validateKanaWasm) {
      const result = window.validateKanaWasm(value);
      setError(result.message);
    }
  };

  const handleShortTextChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setShortText(e.target.value);
  };

  const handleLongTextChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
    setLongText(e.target.value);
  };

  // 文字列置換の実行
  useEffect(() => {
    if (!wasmLoaded || !window.replaceTextWasm) return;

    const result = window.replaceTextWasm(kanaInput, shortText, longText);
    setDisplayText(result.displayText);
    if (result.error) {
      setError(result.error);
    }
  }, [kanaInput, shortText, longText, wasmLoaded]);

  return (
    <div className="input-form">
      <h1>文字列置換フォーム (WebAssembly)</h1>
      <div className="status-indicator">
        {wasmLoaded ? (
          <span className="status-loaded">WebAssembly Ready ✓</span>
        ) : (
          <span className="status-loading">Loading WebAssembly...</span>
        )}
      </div>
      <div className="input-container">
        <label htmlFor="kana">検索する文字（10文字まで）</label>
        <input
          id="kana"
          type="text"
          value={kanaInput}
          onChange={handleKanaInputChange}
          placeholder="置換したい文字列を入力"
        />
        {error && <p className="error-message">{error}</p>}
      </div>
      <div className="input-container">
        <label htmlFor="shortText">置換後の文字（30文字まで）</label>
        <input
          id="shortText"
          type="text"
          value={shortText}
          onChange={handleShortTextChange}
          placeholder="置換後の文字列を入力"
        />
      </div>
      <div className="input-container">
        <label htmlFor="longText">テキスト</label>
        <textarea
          id="longText"
          value={longText}
          onChange={handleLongTextChange}
          placeholder="文章を入力"
          rows={4}
        />
      </div>
      <div className="display-text">
        <h2>変換結果</h2>
        <p>{displayText}</p>
      </div>
    </div>
  );
};

export default InputForm;
```
```tsx:src/ App.tsx
import React from 'react';
import InputForm from './InputForm';

const App: React.FC = () => {
  return (
    <div className="App">
      <InputForm />
    </div>
  );
};

export default App;
```

**wasmのビルド**
ターミナルの`wasm`ディレクトリにて以下のコマンドをコピペしてください！
```
GOOS=js GOARCH=wasm tinygo build -o ../frontend/public/main.wasm -target wasm ./main.go
```
こちらを実行すると`frontend/public`ディレクトリにmain.wasmが追加されます！

`zsh: command not found: tinygo`
もしこのエラーが表示されたらbrewをインストールされているMacOSを使っている方は以下のコマンドをコピペしてみてください
```
brew tap tinygo-org/tools
brew install tinygo
```
これで解決されるはずです！

次にJavaScriptとWasmの通信補助を行うためのコマンドを入力します！
```
cp "$(tinygo env TINYGOROOT)/targets/wasm_exec.js" frontend/public/
```
これで`frontend/public`ディレクトリ内に`wasm_exec.js`が追加されます！

最後に`public/index.html`のheadタグの中に
```
<script src="wasm_exec.js"></script>
```
こちらを追加してください！

これでWebAssemblyを実装したアプリの完成です！

## 実行してみた！
`frontend`ディレクトリに移動して実行してみましょう！
```
npm start
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3631203/0d948291-d976-e71d-4e46-1fc48b575523.png)

うまく実行できました！
それでは「シ」を「ピッピ」に変換するようにして、試しに「新春シャンソンショー(シンシュンシャンソンショー)」で試してみたいと思います！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3631203/2956d093-becf-e05e-5d8a-f9157cbe6468.png)

うまくできました！

## おわりに
以前、投稿した記事にてwasmを使った開発をしてみてもいいのでは？というご意見をいただいたことをきっかけに開発をしてみました！
皆さんもぜひ遊んでみてはいかがでしょうか！

今回の記事はいかがだったでしょうか？
またどこかの記事でお会いしましょう！

## GithubURL

https://github.com/FrohleinYoshie/wasm_karepippi