RuState ドキュメント

RuStateとは

RuStateは、Rustで実装された状態機械(ステートマシン)ライブラリで、WebAssemblyを通じてブラウザからの利用を可能にします。

状態機械は、プログラムが取りうる有限の状態と、その間の遷移を明示的に定義することで、複雑な振る舞いを管理するパターンです。

RuStateは特に以下のような場面で威力を発揮します:

主な特徴

基本的な使い方

1. ステートマシンの定義

Rustで状態と遷移を定義します:

// Rust側のコード
pub enum TrafficLightState {
    Red,
    Yellow,
    Green,
}

pub enum TrafficLightEvent {
    CHANGE,
    RESET,
}

impl StateMachine for TrafficLight {
    fn transition(&mut self, event: TrafficLightEvent) {
        self.state = match (&self.state, event) {
            (TrafficLightState::Red, TrafficLightEvent::CHANGE) => TrafficLightState::Green,
            (TrafficLightState::Green, TrafficLightEvent::CHANGE) => TrafficLightState::Yellow,
            (TrafficLightState::Yellow, TrafficLightEvent::CHANGE) => TrafficLightState::Red,
            (_, TrafficLightEvent::RESET) => TrafficLightState::Red,
        };
    }
}

2. WebAssemblyへのエクスポート

wasm-bindgenを使ってJavaScript向けにサービスを公開します:

#[wasm_bindgen]
pub struct TrafficLightService {
    machine: TrafficLight,
}

#[wasm_bindgen]
impl TrafficLightService {
    pub fn new() -> Self {
        TrafficLightService {
            machine: TrafficLight::new(),
        }
    }
    
    pub fn send(&mut self, event: &str) {
        let event = match event {
            "CHANGE" => TrafficLightEvent::CHANGE,
            "RESET" => TrafficLightEvent::RESET,
            _ => return,
        };
        self.machine.transition(event);
    }
    
    pub fn get_state(&self) -> String {
        format!("{:?}", self.machine.state)
    }
}

3. JavaScript側での利用

ブラウザで以下のようにして利用します:

// WebAssemblyモジュールをロード
wasm_demo.default().then(() => {
    // TrafficLightサービスを初期化
    const trafficLightService = wasm_demo.createTrafficLightService();
    
    // イベントの送信
    trafficLightService.send('CHANGE');
    
    // 現在の状態の取得
    const currentState = trafficLightService.getState();
});

高度な使い方

コンテキスト付きの状態機械

状態だけでなく、追加のデータを持つステートマシンを定義できます:

pub struct CounterMachine {
    state: CounterState,
    context: CounterContext,
}

pub struct CounterContext {
    count: i32,
    max_value: i32,
}

// 状態遷移時にコンテキストも更新
impl StateMachine for CounterMachine {
    fn transition(&mut self, event: CounterEvent) {
        match (&self.state, event) {
            (CounterState::Active, CounterEvent::Increment) => {
                if self.context.count < self.context.max_value {
                    self.context.count += 1;
                } else {
                    self.state = CounterState::MaxReached;
                }
            },
            // 他の遷移...
        }
    }
}

条件付き遷移

遷移にガード条件を設定する例:

fn transition(&mut self, event: UserEvent) {
    self.state = match (&self.state, event) {
        (UserState::LoggedOut, UserEvent::Login) => {
            if self.validate_credentials() {
                UserState::LoggedIn
            } else {
                UserState::Error
            }
        },
        // 他の遷移...
    };
}

API リファレンス

StateMachine トレイト

すべての状態機械の基本となるトレイトです:

pub trait StateMachine {
    type State;
    type Event;
    
    fn transition(&mut self, event: Self::Event);
    fn get_state(&self) -> &Self::State;
    fn can_transition(&self, event: &Self::Event) -> bool;
}

Serviceトレイト

WebAssemblyとして公開するための標準的なインターフェース:

pub trait Service {
    fn send(&mut self, event: &str) -> Result<(), JsValue>;
    fn get_state(&self) -> JsValue;
    fn get_meta(&self) -> JsValue;
}

コアAPI関数一覧

関数名 説明 引数 戻り値
transition 指定されたイベントで状態遷移を実行 event: Event なし
get_state 現在の状態を取得 なし State
can_transition 特定のイベントで遷移可能か検証 event: &Event bool
send 文字列イベントを送信(JS連携用) event: &str Result
get_context コンテキストデータを取得 なし Context
get_meta 状態機械のメタデータを取得 なし JsValue

エラー処理

RuStateでは以下のような方法でエラーを扱います:

// Rust側
pub enum StateMachineError {
    InvalidTransition,
    InvalidState,
    InvalidEvent,
    // その他のエラー
}

// 結果をResultで返す
pub fn send(&mut self, event: &str) -> Result<(), StateMachineError> {
    let parsed_event = self.parse_event(event)?;
    if !self.can_transition(&parsed_event) {
        return Err(StateMachineError::InvalidTransition);
    }
    self.transition(parsed_event);
    Ok(())
}

WebAssembly 連携詳細

wasm-bindgenの使い方

Rustコードをwasmにコンパイルし、JavaScriptから利用可能にするための設定:

// Cargo.toml
[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
wasm-bindgen = "0.2"
js-sys = "0.3"
web-sys = { version = "0.3", features = ["console"] }

[package.metadata.wasm-pack.profile.release]
wasm-opt = ["-Oz"]

型変換

RustとJavaScript間の型変換方法:

// 文字列の変換
#[wasm_bindgen]
pub fn to_uppercase(input: &str) -> String {
    input.to_uppercase()
}

// 複合データ型の変換
#[wasm_bindgen]
pub fn get_machine_state(&self) -> JsValue {
    let state = self.machine.get_state();
    let context = self.machine.get_context();
    
    let obj = js_sys::Object::new();
    js_sys::Reflect::set(&obj, &"state".into(), &state.to_string().into()).unwrap();
    js_sys::Reflect::set(&obj, &"count".into(), &context.count.into()).unwrap();
    obj.into()
}

非同期処理

WebAssembly内での非同期処理の扱い方:

#[wasm_bindgen]
pub async fn async_operation() -> Result {
    // 非同期処理の実装
    let result = perform_async_task().await?;
    Ok(result.into())
}

// JavaScript側での利用
machine.asyncOperation().then(result => {
    console.log("Operation completed:", result);
});

メモリ管理の最適化

WebAssemblyとJavaScript間のメモリ共有と最適化テクニック:

実装パターン集

複数の状態機械の連携

大規模アプリケーションでは、複数の小さな状態機械を組み合わせることが効果的です:

pub struct AppService {
    auth_machine: AuthMachine,
    form_machine: FormMachine,
    navigation_machine: NavigationMachine,
}

#[wasm_bindgen]
impl AppService {
    pub fn send_to_auth(&mut self, event: &str) -> Result<(), JsValue> {
        self.auth_machine.send(event)
    }
    
    pub fn send_to_form(&mut self, event: &str) -> Result<(), JsValue> {
        self.form_machine.send(event)
    }
    
    // イベントに応じて適切な状態機械にルーティング
    pub fn send(&mut self, target: &str, event: &str) -> Result<(), JsValue> {
        match target {
            "auth" => self.send_to_auth(event),
            "form" => self.send_to_form(event),
            // その他...
            _ => Err("Invalid target".into()),
        }
    }
}

Reactとの連携パターン

React.jsでRuStateを利用する効果的な方法:

// React Custom Hook
function useStateMachine(initialMachine) {
  const [state, setState] = useState(initialMachine.getState());
  const machineRef = useRef(initialMachine);
  
  const send = useCallback((event) => {
    machineRef.current.send(event);
    setState(machineRef.current.getState());
  }, []);
  
  return [state, send];
}

// コンポーネント内での利用
function TrafficLight() {
  const [machine, setMachine] = useState(null);
  const [state, send] = useStateMachine(machine);
  
  useEffect(() => {
    wasm_module.default().then(() => {
      setMachine(wasm_module.createTrafficLightMachine());
    });
  }, []);
  
  if (!machine) return 
Loading...
; return (
); }

状態の永続化

ステートマシンの状態を保存・復元する方法:

#[wasm_bindgen]
impl MachineService {
    // 状態のシリアライズ
    pub fn serialize(&self) -> String {
        serde_json::to_string(&self.machine).unwrap_or_default()
    }
    
    // 状態の復元
    pub fn deserialize(data: &str) -> Result {
        let machine: Machine = serde_json::from_str(data)
            .map_err(|e| JsValue::from_str(&e.to_string()))?;
        
        Ok(MachineService { machine })
    }
}

// JavaScript側での利用
// 状態の保存
localStorage.setItem('machineState', machine.serialize());

// 状態の復元
const savedState = localStorage.getItem('machineState');
if (savedState) {
    const machine = MachineService.deserialize(savedState);
}

プロジェクト構成

RuStateを使ったプロジェクトの基本構成:

rustate/
├── Cargo.toml
├── src/
│   ├── lib.rs        # メインライブラリコード
│   ├── machines/     # 状態機械の実装
│   └── bindings.rs   # WebAssembly向けバインディング
└── web/
    ├── index.html    # デモページ
    ├── docs.html     # このドキュメント
    ├── style.css     # スタイルシート
    └── pkg/          # ビルドされたwasmモジュール

ビルドとデプロイ

環境設定

必要なツール:

ビルド手順

# WebAssemblyとしてビルド
wasm-pack build --target web

# 開発サーバーを起動(オプション)
npx serve .

CI/CDパイプラインの例

GitHub Actionsを使った継続的インテグレーション・デプロイメントの設定例:

name: Build and Deploy

on:
  push:
    branches: [ main ]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Setup Rust
        uses: actions-rs/toolchain@v1
        with:
          profile: minimal
          toolchain: stable
      - name: Install wasm-pack
        run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
      - name: Build
        run: wasm-pack build --target web
      - name: Deploy to GitHub Pages
        uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./web

パフォーマンスとデバッグ

パフォーマンス考慮事項

RuStateで効率的なアプリケーションを構築するためのヒント:

デバッグツール

wasm-pack testコマンドを使用して、WebAssemblyコードのテストを実行できます。

開発時は、console.logでの状態出力や、chromeのWebAssemblyデバッグツールも活用してください。

状態遷移の可視化

状態遷移をログとして出力し、デバッグを容易にする方法:

#[wasm_bindgen]
impl MachineService {
    pub fn send_with_logging(&mut self, event: &str) -> Result<(), JsValue> {
        let prev_state = self.get_state();
        let result = self.send(event);
        
        if result.is_ok() {
            let new_state = self.get_state();
            web_sys::console::log_3(
                &"Transition:".into(),
                &format!("{} --[{}]--> {}", prev_state, event, new_state).into(),
                &self.get_context().into()
            );
        }
        
        result
    }
}

よくある質問

RuStateは他のステートマシンライブラリとどう違いますか?
RuStateは特にWebAssemblyとの連携に重点を置いており、Rustの型安全性を最大限に活用しています。
大規模なアプリケーションでも利用できますか?
はい。複数の小さな状態機械を組み合わせることで、大規模なアプリケーションも構築できます。
Redux/XStateなどのJavaScriptライブラリと併用できますか?
はい。WebAssemblyを通じて公開されるAPIを利用して、既存のJavaScriptフレームワークと統合できます。
パフォーマンスはJavaScript実装と比べてどうですか?
一般的に、計算量の多い処理はWebAssembly版の方が高速ですが、DOM操作などJavaScriptとの頻繁なやり取りが必要な場合は、オーバーヘッドにより差が縮まることがあります。
どのようなユースケースに最適ですか?
複雑なビジネスロジック、計算集約型の処理、厳格な型安全性が求められるアプリケーションに最適です。特に、ユーザーインターフェース、ワークフロー管理、ゲームロジックなどに適しています。
テスト方法はどうすればよいですか?
Rustの単体テストとwasm-packのテスト機能を組み合わせることで、効率的にテストできます。状態機械の各遷移を個別にテストし、エッジケースも確認することをお勧めします。