プログラミング
Cubacadabraの背後にある魔法
The Magic Behind Cubacadabra (andrewarrow.dev)
要約
この記事は、Cubacadabraというプロジェクトが、複数のプラットフォーム(iOS、Android、Web)で共通のロジックをRustで共有するアプローチについて説明しています。これにより、コードの重複を避け、メンテナンス性を向上させ、一貫したユーザー体験を提供することを目指しています。ネイティブUIは各プラットフォームに最適化しつつ、アプリケーションのコアな意思決定ロジックはRustに集約することで、開発効率と品質の向上を図っています。
全文翻訳
01 / コードを消す
abracadabraがトップハットの上で言う言葉になる前は、首に巻くものでした。Quintus Serenus Sammonicusに帰せられる古代ローマのLiber Medicinalisでは、処方は言葉を繰り返し書き、文字を一つずつ削除して、それを熱病に対するお守りとして身につけることでした。問題を小さくする、非常に文字通りのアプローチです。
iOS、Android、Web版の何かを出荷したことがあるなら、通常のルーチンを知っているでしょう。Swiftで機能をビルドします。Kotlinで再度ビルドします。JavaScriptで再度ビルドします。各バージョンには独自の検証、リクエスト、ローディング状態、エラー処理があります。それらは同じバックエンドと通信するため、私たちはそれらを1つの製品の3つのクライアントと呼びます。それでも、私たちは製品の多くを3回書きました。それから機能が変更されます。iOSは保存の失敗から回復する方法を学びます。Androidは1週間後に修正を受け取ります。ブラウザは同じエラーを異なる方法で処理します。誰も3つの動作を設計しようとしたわけではありません。それらは、誰もが通常の開発を行っている間に蓄積されただけです。
私はcubacadabraのクライアントがRustの薄いシェルになることを望んでいます。ネイティブコントロール、デバイスサービス、ブラウザ統合に必要な最小限のSwift、Kotlin、JavaScriptを維持します。ポータブルなアプリケーションロジックを共有Rustクレートに移動します。これには、退屈なアカウント画面だけでなく、ゲームエンジンも含まれます。DRY(Don't Repeat Yourself)は、アプリケーションが行う決定に適用されるべきです。
ユーザー名フィールドは、依然としてSwiftUIのテキストフィールド、Composeのテキストフィールド、またはHTML入力になる可能性があります。それぞれがRustに編集を転送し、結果の状態を表示します。Rustは、保存が有効かどうか、どのリクエストを行うか、その応答が何を意味するかを決定します。ルールを変更すると、1つの実装が変更されます。3つの画面は、プラットフォームに属しているように見え続けることができます。
良い先例があります。Litterは、セッション状態、ストリーミング、再接続動作を所有するRustコアの上にネイティブのiOSおよびAndroidインターフェースを持っています。これはUniFFIバインディングを介して公開されています。Mozillaの共有Rustコンポーネントは、Firefoxのデスクトップおよびモバイルアプリの個別の同期実装を維持することから成長しました。理由は馴染み深いでしょう。重複したロジックの保守が困難であり、実装間の違いがバグを引き起こしました。
02 / これらすべてに保存ボタンの価値はありますか?
私は長いアーキテクチャの議論とiOS開発ノートを通じてこれを検討しました。経験豊富なモバイル開発者は、なぜユーザー名フィールドにRust、C ABI、JNI、およびWASMが必要なのかと合理的に尋ねるかもしれません。個別のネイティブ実装は、独自のIDEでデバッグするのが簡単です。バインディングはビルド作業とライフタイムのバグを追加します。少数のフォームを持つ小さなアプリの場合、重複の方が安価かもしれません。
cubacadabraについては、共有コアの価値を確信しています。RustはすでにエンジンとデスクトップStudioを実行しており、ブラウザはすでにWASMを介してそれを実行しています。プラットフォームが成長するにつれて、ゲームへの参加には、コンテンツの互換性、ペアレンタルコントロール、サブスクリプションアクセス、ブロックされたプレイヤー、および接続が途中で切れた場合の回復が含まれます。これらのインタラクションを一度だけ解決したいです。新しいルールごとに3つの実装間の調整問題になることを望みません。
投資は成果を上げ始めています。1つのiOS統合コミットは106行を追加し、269行を削除し、ユーザー名画面とそのブリッジを簡素化しました。最初のWeb統合は、インフラストラクチャが必要だったため成長しました。将来の機能は、その機械を再利用するはずです。テストは、それらの複雑な動作が1つの場所にあり、ホストアダプターが小さいままであるかどうかです。
03 / 保存ボタンには意見があります
1つの実装は、1つの巨大なクレートを意味するわけではありません。cubacadabra-clientは、エンジンに面したマルチプレイヤーセッションを所有します。cubacadabra-appは、ゲームプレイ外のポータブルな動作を所有します。エンジンはシミュレーションとレンダリングを所有します。ユーザー名の保存は、3つのコンシューマークライアントすべてが使用できるアプリクレートに属します。
たとえば、「Dragon_7」を保存し、入力を続け、応答が到着する前にサインアウトしたとします。古い応答が新しいドラフトを上書きしたり、次にサインインしたユーザーを更新したりする可能性があります。これはまさに私が一度だけ修正したいルールの種類です。
ホストはUsernameChangedとSaveUsernameをディスパッチします。Rustはドラフトを検証し、ID、アカウントID、メソッド、パス、ボディを持つHTTPエフェクトを発行します。ホストは認証情報を提供し、リクエストを実行し、生のステータスとボディを返します。Rustは、画面がプロファイルを更新する前に応答を受け入れるか拒否します。HTTPエグゼキューターはネイティブのままで、リクエストの意味は共有されたままです。セッションの置き換えは、保留中の作業を無効にします。エフェクトIDはモデル内で再利用されないため、遅延した応答が新しいアカウントの操作を完了することはできません。保存中の編集は、新しいドラフトを保持します。これらの決定には、1つの実装と1セットの共有契約ケースがあります。
アバターの保存、カタログのページネーション、楽観的なブロック/アンブロックは、現在同じパターンに従っています。レポートはホスト所有のままです。電話では、AppViewModelがGameViewModelの隣に配置されているため、ゲームの再起動はアカウントの保存をキャンセルしません。ランタイム契約は、その境界とそのテストを文書化します。
04 / そのポインタの所有者は誰ですか?
私たちのバインディングはLitterのものとは異なります。cubacadabraは現在、モバイルでは小さなCインターフェースを使用し、ブラウザではwasm-bindgenを使用しています。アプリクレートにはSerdeとserde_jsonが必要ですが、レンダラーや非同期HTTPランタイムはありません。これにより、ゲームを開始せずに共有アプリケーションの動作を使用できます。
ネイティブアプリABIは7つの関数です。create、destroy、dispatch JSON、get snapshot JSON、poll effect JSON、およびoutput pointerとlengthの読み取り。ハンドルはシングルスレッドです。出力はRustに属し、次のミューテーションの前にコピーする必要があります。SwiftはそれをDataにコピーします。Androidには小さなJNIバイト配列アダプターがあります。両方ともスナップショットプロトコルバージョンをチェックします。互換性のないバインディングは、静かに第2セットのアカウントルールを復活させる代わりに失敗します。
iPhone / ネイティブクライアント
ホストはデバイスサーフェスを提供します。共有ランタイムは、ワールド、コントロール、プレイヤー状態を提供します。シリアライゼーションとコピーがあります。アカウント編集やカタログページについては、契約を読むことができるのが好きです。Studioは、通常のRustメソッドと列挙型を介してゲームクライアントを呼び出します。ネイティブゲームクライアントは、既存の入力およびレンダリングAPIのために、借用されたエンジンポインタを公開します。クライアントを破棄すると、それが無効になります。Rustの所有権ルールは、次の呼び出し元がCである場合に、正直な説明を必要とします。
ブラウザには、もう1つの簡単な間違いが待っています。WebClientとWebRendererは同じ生成されたWASMモジュールから来ており、エンジンハンドルは両側で同じ線形メモリを参照します。同じソースからコンパイルされた2つのモジュールは、ポインタを相互に交換可能にしません。アプリランタイムは個別のWASMモジュールであり、アカウントページがレンダラーをロードせずにユーザー名を編集できるようにします。共有するエンジンポインタはありません。
05 / 2人が同じものを触った
クライアントの抽出は、別の種類の繰り返しを除去しました。各ホストは、ソケットメッセージを翻訳し、リモートプレイヤーを維持し、ワールド変更をルーティングし、Luauネットワークアウトボックスをドレインしていました。ブラウザの「crate 2nd」コミットでは、244行が削除され、74行が追加されました。これはホストの差分であり、アダプターが含まれています。有用な部分は、修正するセッション実装が1つあることです。
Studio / テストワークスペース
マルチプレイヤーサーフェスは、「1つのクライアントが機能する」と「セッションが機能する」との間のギャップを無視できないものにします。ホストはソケットテキストをClientSession::receive_textに渡し、エンジンをステップ実行する前後にアクションをポーリングします。RustはSetWorldとSendTextを発行します。ホストは依然として認証、再接続バックオフ、および移動送信スロットリングを所有します。起動は、ソケットを「arena:7」にルーティングしながら、エンジンの論理ワールドを「arena」として保持できます。RustはリモートIDと世代も追跡し、無視されたアカウントをフィルタリングし、変更時にバージョン化された名簿を送信します。
ここで、2人の友人を同じリレーチェックポイントに配置します。両方ともシーケンス7を読み取ります。両方ともラウンドを進めようとします。JavaScriptバックエンドは