ホルマリンなしの実際の図をブロックします

ブロック図は、複雑なアルゴリズムを理解しやすく構造化されたアクションシーケンスに変えるのに役立つ視覚ツールです。プログラミングからビジネスプロセス管理まで、最も複雑なシステムの視覚化、分析、最適化のための普遍的な言語として機能します。

道路の代わりにロジックであり、都市の代わりにアクションがある地図を想像してください。これはブロック図であり、最も混乱するプロセスでナビゲーションのための不可欠なツールです。

例1:簡素化されたゲームの起動スキーム
仕事の原則を理解するために、簡単なゲームの起動スキームを提示しましょう。

このスキームは、すべてが失敗することなく起こるときの完璧なスクリプトを示しています。しかし、実際の生活では、すべてがはるかに複雑です。

例2:データ読み込みでゲームを開始するための拡張スキーム
最新のゲームでは、ユーザーデータ、保存、または設定をダウンロードするためにインターネット接続が必要になることがよくあります。これらの手順をスキームに追加しましょう。

このスキームはすでにより現実的ですが、何かがうまくいかない場合はどうなりますか?

どうでしたか:インターネットの損失で「壊れた」ゲーム

プロジェクトの開始時に、開発者は考えられるすべてのシナリオを考慮することができませんでした。たとえば、彼らはゲームの主な論理に焦点を合わせており、プレーヤーにインターネット接続がある場合に何が起こるか考えませんでした。

このような状況では、彼らのコードのブロック図は次のようになります。

この場合、エラーを発行したり正しく閉じたりする代わりに、接続がないために受け取っていないデータを待つ段階でゲームが凍結しました。これにより、「ブラックスクリーン」が発生し、アプリケーションが凍結されました。

それがどのようになったか:ユーザーの苦情の修正

ホバリングに関する多くのユーザーの苦情の後、開発者チームは、エラーを修正する必要があることに気付きました。彼らは、アプリケーションが接続の欠如に正しく応答できるエラー処理ユニットを追加することにより、コードを変更しました。

これは、修正されたブロック図がどのように見えるかであり、両方のシナリオが考慮されます。

このアプローチのおかげで、ゲームはユーザーに問題について正しく通知し、場合によってはオフラインモードに移動してゲームを続けることができます。これは、ブロック図が非常に重要である理由の良い例です:開発者に、実行の理想的な方法だけでなく、すべての可能な障害についても考えさせ、最終製品をより安定して信頼できるものにします。

不確実な行動

ハンギングとエラーは、プログラムの予測不可能な動作の例の1つにすぎません。プログラミングでは、不確実な動作(未定義の行動)の概念があります – これは、言語の基準が特定のケースでプログラムの動作を説明しない状況です。

これは何にもつながる可能性があります:撤退のランダムな「ごみ」からプログラムの失敗や深刻なセキュリティの脆弱性まで。たとえば、メモリを使用して作業するとき、無期限の動作はしばしば発生します。たとえば、Cの言語の線で

言語cの例:

開発者がラインをバッファーにコピーしたが、ゼロシンボル( `\ 0`)を最後に追加するのを忘れたと想像してください。

これがコードがどのように見えるかです:

#include 

int main() {
char buffer[5];
char* my_string = "hello";

memcpy(buffer, my_string, 5);

printf("%s\n", buffer);
return 0;
}

予想される結果:「こんにちは」
実際の結果は予測不可能です。

なぜこれが起こっているのですか? spitifier`%sを使用した「printf」関数は、線がゼロシンボルで終了することを期待しています。彼がそうでない場合、彼女はハイライトされたバッファーの外でメモリを読み続けます。

次に、このプロセスのブロック図を紹介します。

これは、ブロック図が非常に重要である理由の明確な例です。開発者に、理想的な実行方法だけでなく、そのような低レベルの問題を含むすべての可能な障害についても考えさせ、最終製品のより安定性と信頼性を高めます。

Pixel Perfect:宣言性の時代の神話または現実?

インターフェイス開発の世界では、共通の概念があります – 「ロッジに完璧なピクセル」。これは、デザインマシンの最小ピクセルへの最も正確な複製を意味します。長い間、それは、特に古典的なウェブデザインの時代において、ゴールドスタンダードでした。しかし、宣言的なマイルの到着とさまざまなデバイスの急速な成長により、「Pixel Perfect」の原則はより一時的になりつつあります。理由を把握してみましょう。

Imperial Wysiwyg vs.宣言コード:違いは何ですか?

従来、多くのインターフェイス、特にデスクトップは、編集者の命令的なアプローチまたはwysiwyg(あなたが見るものです)を使用して作成されていました。このようなツールでは、デザイナーまたは開発者は要素を直接操作し、ピクセルに正確にキャンバスに配置します。それはグラフィックエディターとの作業に似ています – あなたはあなたの要素がどのように見えるかを見ることができ、あなたは間違いなくそれを配置することができます。この場合、「Pixel Perfect」の達成は非常に現実的な目標でした。

ただし、現代の開発は、宣言マイルにますます基づいています。これは、コンピューターに「このボタンをここに置く」ように指示するのではなく、取得したいものを説明することを意味します。たとえば、要素の特定の座標を示す代わりに、そのプロパティについて説明します。「このボタンは赤く、すべての側面から16pxのインデントを持ち、容器の中心にある必要があります。」 Freimvorkiは、React、Vue、Swiftui、Jetpackのようなものです。この原則を使用するだけです。

なぜ「Pixel Perfect」が多くのデバイスで宣言的なマイルで動作しない

iPhone 15 Pro Max、Samsung Galaxy Fold、iPad Pro、および4K解像度で等しく見栄えの良いアプリケーションを作成すると想像してください。これらの各デバイスには、画面解像度、ピクセル密度、パーティ、および物理サイズが異なります。

宣言的アプローチを使用する場合、システム自体は、すべてのパラメーターを考慮して、特定のデバイスに説明されたインターフェイスを表示する方法を決定します。厳しい座標ではなく、ルールと依存関係を設定します。

* 適応性と応答性:宣言マイルの主な目標は、適応的で応答性のあるインターフェイスを作成することです。これは、読みやすさを壊して維持せずに、インターフェイスが画面のサイズと方向に自動的に適応する必要があることを意味します。各デバイスで「Pixel Perfect」を試みた場合、同じインターフェイスの無数のオプションを作成する必要があります。
* ピクセル密度(DPI/PPI):デバイスのピクセル密度は異なります。スケーリングを考慮しない場合、高密度のデバイス上の100の「仮想」ピクセルのピクセルの同じ要素は、低密度デバイスよりもはるかに小さく見えます。宣言的なフレームワークは、物理的なピクセルによって抽象化され、論理ユニットを使用します。
* 動的コンテンツ:最新のアプリケーションのコンテンツは、しばしば動的です – その体積と構造は異なる場合があります。ピクセルに激しくぼろぼろにした場合、テキストや画像の変更はレイアウトの「崩壊」につながります。
* さまざまなプラットフォーム:さまざまなデバイスに加えて、異なるオペレーティングシステム(iOS、Android、Web、デスクトップ)があります。各プラットフォームには、独自の設計、標準コントロール、フォントがあります。すべてのプラットフォームで完全に同一のピクセル完璧なインターフェイスを作成しようとすると、不自然なタイプとユーザーエクスペリエンスの低下につながります。

古いアプローチは消えませんでしたが、進化しました

インターフェイスへのアプローチは、「必須」と「宣言」の間のバイナリ選択ではないことを理解することが重要です。歴史的に、各プラットフォームには、インターフェイスの作成に対する独自のツールとアプローチがありました。

* ネイティブインターフェイスファイル: iOSの場合は、Android-XMLマーキングファイルのXIB/ストーリーボードでした。これらのファイルは、ピクセルに最適なWysiWygレイアウトであり、エディターと同様に無線に表示されます。このアプローチはどこにも消えておらず、開発を続けており、最新の宣言的なフレームと統合されています。たとえば、AppleとJetpackのSwiftuiはAndroidで構成され、純粋に宣言的なコードのパスで出発しましたが、同時に古典的なレイアウトを使用する機会を保持しました。
* ハイブリッドソリューション:多くの場合、実際のプロジェクトでは、アプローチの組み合わせが使用されます。たとえば、アプリケーションの基本構造は宣言的に実装できます。特定のために、要素の正確な位置決めを必要とするため、プラットフォームの詳細を考慮して、より低レベルの命令的な方法を使用するか、ネイティブコンポーネントを開発できます。

モノリスから適応性への:デバイスの進化が宣言的なマイルを形成する方法

デジタルインターフェイスの世界は、過去数十年にわたって大きな変化を遂げてきました。固定許可を持つ固定コンピューターから、さまざまなユーザーデバイスの指数関数的な成長の時代になりました。今日、当社のアプリケーションは次のことで等しくうまく機能するはずです

* すべてのフォームファクターと画面サイズのスマートフォン
* タブレット独自の方向モードと分離画面があります。
* ラップトップとデスクトップさまざまな許可証のモニター。
* テレビとメディアセンター、リモートで制御されています。テレビでさえ、最小限のボタンを備えたApple TVリモート
のように単純なものになる可能性があることは注目に値します。インターフェイスは、特定のリモートコントロールと対話する「方法」を追加することなく、「まるでそれ自体のように」機能するはずです。
* ミニマルな画面を備えたスマートウォッチとウェアラブルデバイス
* 仮想現実ヘルメット(VR)。空間インターフェイスに対するまったく新しいアプローチが必要です。
* 拡張現実デバイス(AR)、現実世界に関する情報を適用します。
* 自動車情報およびエンターテイメントシステム
*そして、家電製品:感覚スクリーンを備えた冷蔵庫と、スマートオーブンとスマートハウスのシステムへのインタラクティブなディスプレイを備えた洗濯機から。

これらの各デバイスには、物理的寸法、パーティー比、ピクセル密度、入力方法(タッチスクリーン、マウス、コントローラー、ジェスチャー、ボーカルコマンド)、および重要なことに、ユーザー環境の微妙さ。たとえば、VR Shleshには深い浸漬が必要であり、外出先でのスマートフォンの高速で直感的な作業が必要ですが、冷蔵庫のインターフェイスは簡単で大きくて、迅速なナビゲーションを使用する必要があります。

古典的なアプローチ:個々のインターフェイスをサポートする負担

デスクトップの優位性と最初のモバイルデバイスの時代において、通常のビジネスは、個々のインターフェイスファイルのの作成とサポートでした。

* iOS に基づく開発は、xcodeでストーリーボードまたはxibファイルを使用する必要があることが多く、Objective-cまたはswiftのコードを作成しました。
* android XMLマーキングファイルとJavaまたはKotlinのコードが作成されました。
* WebインターフェイスはHTML/CSS/JavaScriptをオンにしました。
* c ++アプリケーションの場合さまざまなデスクトッププラットフォームで、特定のフレームワークとツールが使用されました。
* Windows では、これらはMFC(Microsoft Foundationクラス)、手動描画要素を備えたWin32 API、またはダイアログウィンドウとコントロール要素のリソースファイルを使用していました。
*グラフィックインターフェイスを直接制御するためのココア(Objective-C/Swift)または古いカーボンAPIが macos で使用されました。
* linux/unixのようなシステムでは、GTK+やQTなどのライブラリがよく使用され、XMLのようなマーキングファイル(QTデザイナーの.UIファイルなど)または要素の直接ソフトウェアの作成を介して、インターフェイスを作成するためのウィジェットとメカニズムのセットを提供しました。

このアプローチにより、各プラットフォームを最大限に制御できるようになり、特定のすべての機能とネイティブ要素を考慮することができます。しかし、彼には大きな欠点がありました。努力の複製とサポートのための途方もないコスト。設計または機能のわずかな変更には、実際には独立したコードベースがいくつかの権利を導入する必要がありました。これは、開発者チームにとって本当の悪夢になり、新しい機能の出力を遅くし、エラーの可能性を高めました。

宣言マイル:多様性のための単一言語

宣言マイルが支配的なパラダイムとして登場したのは、この急速な複雑さに対応していました。 React、Vue、Swiftui、Jetpackのようなframwsはを構成します。

宣言的アプローチの主なアイデア:システムがすべての要素を描画する方法(命令)を「どのように」(命令したいか(宣言)」と説明する代わりに。インターフェイスのプロパティと条件を設定し、フレームワークは特定のデバイスに最適に表示する方法を決定します。

これは、次の重要な利点のおかげで可能になりました。

1。プラットフォームの詳細からの抽象化:宣言的なfraimvorkiは、各プラットフォームの低レベルの詳細を忘れるように特別に設計されています。開発者は、単一の転送されたコードを使用して、より高いレベルの抽象化でコンポーネントとその関係を説明します。
2。自動適応と応答性: freimvorki 自動スケーリング、要素のレイアウト、およびさまざまなサイズの画面、ピクセル密度、入力方法へのレイアウトと適応の変更について責任を負います。これは、FlexBoxやグリッドなどの柔軟なレイアウトシステムの使用、および「論理ピクセル」や「DP」に似た概念を使用して達成されます。
3。ユーザーエクスペリエンスの一貫性:外部の違いにもかかわらず、宣言的アプローチにより、デバイスのファミリー全体で行動と相互作用の単一のロジックを維持できます。これにより、テストプロセスが簡素化され、より予測可能なユーザーエクスペリエンスが提供されます。
4。開発とコスト削減の加速:多くのプラットフォームで作業できるのと同じコードを使用すると、開発とサポートの時間とコストによってが大幅に削減されます。チームは、同じインターフェイスを繰り返し書き直すことではなく、機能と設計に焦点を合わせることができます。
5。 Freimvorkiは新しいテクノロジーをサポートするために更新でき、すでに書かれたコードがこのサポートを受け取ることは比較的シームレスです。

結論

宣言的なマイルは、単なるファッションのトレンドではなく、モノのインターネット(IoT)やスマート世帯家電などのユーザーデバイスの急速な開発によって引き起こされる必要な進化ステップです。これにより、開発者と設計者は、各プラットフォームの無限の特定の実装にownれなくなることなく、複雑で適応的で均一なインターフェイスを作成できます。各ピクセルの命令制御から目的の状態の宣言的記述への遷移は、将来のインターフェイスの世界では、表示される画面に関係なく、柔軟で、転送され、直感的であるであるべきであるという認識です。

プログラマー、デザイナー、ユーザーは、この新しい世界で生きる方法を学ぶ必要があります。 特定のデバイスまたは解像度に合わせて設計されたPixel Perfectの追加の詳細は、開発とサポートのために不必要な時間コストにつながります。さらに、このような過酷なレイアウトは、限られた入力テレビ、VR、ARシフトなどの標準以外のインターフェイスを備えたデバイスや、将来の他のデバイスでも機能しない場合がありますが、今日でもわかりません。柔軟性と適応性 – これらは、現代世界で成功したインターフェイスを作成するための鍵です。

なぜプログラマーはニューラルネットワークを使用しても何もしません

今日、ニューラルネットワークはどこでも使用されています。プログラマーはそれらを使用して、コードを生成し、他のソリューションを説明し、ルーチンタスクを自動化し、アプリケーション全体をゼロから作成します。これにより、効率の向上、エラーの減少、開発の加速につながるはずです。しかし、現実ははるかに普及しています。多くはまだ成功していません。ニューラルネットワークは重要な問題を解決しません – 無知の深さを照らすだけです。

理解する代わりにLLMへの完全な依存性

主な理由は、多くの開発者がLLMに完全に依存しており、彼らが働いているツールを深く理解する必要性を無視していることです。ドキュメントを勉強する代わりに、チャットリクエスト。エラーの理由を分析する代わりに、決定をコピーします。建築ソリューションの代わりに、説明に従ってコンポーネントの生成。これはすべて表面的なレベルで機能しますが、非標準タスクが発生するとすぐに、実際のプロジェクトとの統合または微調整の必要性が必要であり、すべてが崩れます。

コンテキストと時代遅れの慣行の欠如

ニューラルネットワークは、一般化されたコードを生成します。彼らは、あなたのプラットフォームの詳細、図書館のバージョン、環境制限、またはプロジェクトの建築ソリューションを考慮していません。生成されるものはしばしばもっともらしいように見えますが、実際のサポートされているコードとは何の関係もありません。それらが時代遅れのバージョンのフレームワークに属している場合、または長い間効果のないまたは安全でないと認識されてきたアプローチを使用する場合、単純な推奨事項でさえも機能しない場合があります。モデルはコンテキストを理解していません – 統計に依存しています。これは、オープンコードで人気のあるエラーとアンチパットターンが何度も何度も再現されることを意味します。

冗長性、非効率性、プロファイリングの欠如

生成されたAIを生成するコードは、多くの場合冗長です。不要な依存関係、ロジックの複製、抽象化を不必要に追加します。サポートが困難な効果的で重い構造が判明しました。これは、ギャングのサイズ、応答時間、エネルギー消費が重要であるモバイル開発では特に深刻です。

ニューラルネットワークはプロファイリングを実施せず、CPUとGPUの制限を考慮していないため、メモリの漏れを気にしません。コードが実際にどれほど効果的であるかを分析しません。最適化は依然として手作りであり、分析と検査が必要です。それがなければ、アプリケーションは、構造の観点から「正しい」ように見える場合でも、遅く、不安定でリソース集中的になります。

脆弱性とセキュリティに対する脅威

安全を忘れないでください。 LLMを使用して部分的または完全に作成されたプロジェクトが正常にハッキングされた場合、すでに既知のケースがあります。理由は典型的なものです。危険な関数の使用、入力データの検証の欠如、承認の論理のエラー、外部依存関係の漏れです。ニューラルネットワークは、オープンリポジトリで見つかったという理由だけで、脆弱なコードを生成できます。セキュリティスペシャリストの参加と完全な改訂がなければ、このようなエラーは攻撃の入力ポイントに簡単になります。

法律はパレートであり、欠陥の本質

パレートの法律は、ニューラルネットワークではっきりと機能します。努力の20%のために結果の80%が達成されます。このモデルは、大量のコードを生成し、プロジェクトの基礎を作成し、構造を広げ、タイプをアレンジし、モジュールを接続できます。ただし、これはすべて時代遅れであり、現在のバージョンのライブラリまたはフレームワークと互換性があり、重要な手動改訂が必要です。ここでの自動化は、プロジェクトの特定の現実にチェックされ、処理され、適応する必要があるドラフトとして機能します。

注意楽観主義

それにもかかわらず、未来は励みに見えます。トレーニングデータセットの絶え間ない更新、現在のドキュメントとの統合、自動化されたアーキテクチャチェック、設計およびセキュリティパターンへのコンプライアンス – これがすべてゲームのルールを根本的に変更する可能性があります。おそらく数年後には、コードをより速く、より安全で、より効率的に書くことができ、LLMに真の技術的な共著者として依存することができます。しかし、今のところ – 悲しいかな – 多くのことをチェックし、書き直し、手動で修正する必要があります。

ニューラルネットワークは強力なツールです。しかし、彼があなたに反対するのではなく、あなたのために働くためには、いつでもコントロールするための基盤、批判的思考、意欲が必要です。

バイブコアトリック:なぜLLMが固体、乾燥、きれいに動作しないのか

CHATGPTなどの大規模な言語モデル(LLM)の開発により、ますます多くの開発者がそれらを使用してコードを生成し、設計アーキテクチャを生成し、統合を加速します。ただし、実用的なアプリケーションでは、それは顕著になります。建築の古典原則 – 固体、乾燥、清潔 – LLMコドゲンデーションの特性とうまくやっていません。

これは、原則が時代遅れであることを意味するものではありません – それどころか、それらは手動開発と完全に連携しています。しかし、LLMでは、アプローチを適応させる必要があります。

なぜLLMが建築原則に対処できないのか

カプセル化

インクシレーションでは、システムの一部間の境界、開発者の意図に関する知識を理解し、厳格なアクセス制限に従う必要があります。 LLMは、多くの場合、構造を簡素化したり、理由もなくフィールドを公開したり、実装を複製したりします。これにより、コードはエラーに対してより脆弱になり、アーキテクチャの境界に違反します。

要約とインターフェイス

抽象的な工場や戦略などの設計パターンには、システムの全体的な見方が必要であり、そのダイナミクスを理解する必要があります。モデルは、実装を保証せずに明確な目的なしでインターフェイスを作成したり、レイヤー間の接続に違反したりできます。結果は、過剰または非機能的アーキテクチャです。

dry(donolt繰り返し自分で繰り返します)

LLMは、繰り返しコードを最小限に抑えようとはしません – それどころか、一般的なロジックを作成するよりもブロックを複製する方が簡単です。リクエストに応じてリファクタリングを提供できますが、デフォルトでは、たとえ冗長性につながる場合でも、「自己適切な」フラグメントを生成する傾向があります。

クリーンアーキテクチャ

Cleanは、厳格な階層、フレームワークからの独立性、指向性の依存性、およびレイヤー間の最小のつながりを意味します。このような構造の生成には、システムのグローバルな理解が必要です。また、LLMは、建築の完全性ではなく、単語の確率のレベルで機能します。したがって、コードは混合され、依存の方向に違反し、レベルへの単純化された分割が違反されます。

LLMを使用するときにうまく機能する

乾燥する代わりに濡れています
WET(すべてを2回書く)アプローチは、LLMでの作業においてより実用的です。コードの複製は、保持のモデルからのコンテキストを必要としません。つまり、結果は予測可能であり、正しく修正しやすくなります。また、非自明なつながりやバグの可能性を減らします。

さらに、複製はモデルの短いメモリを補うのに役立ちます。いくつかの場所でロジックの特定の断片が見つかった場合、LLMはそれをさらなる生成で考慮に入れる可能性が高くなります。これにより、伴奏が簡素化され、「忘却」に対する抵抗が増加します。

カプセル化の代わりに単純な構造

複雑なカプセル化を回避し、コードの部分間のデータの直接送信に依存すると、生成とデバッグの両方を大幅に簡素化できます。これは、MVPの迅速な開発または作成で特に当てはまります。

簡略化されたアーキテクチャ

プロジェクトのシンプルでフラットな構造は、最小量の依存関係と抽象化を備えており、発電中はより安定した結果をもたらします。モデルはこのようなコードを簡単に適応させ、コンポーネント間の予想される接続に違反することが多い場合があります。

SDK統合 – 手動で信頼できる

ほとんどの言語モデルは、時代遅れのバージョンのドキュメントでトレーニングされています。したがって、SDKをインストールするための手順を生成する場合、エラーがよく表示されます。時代遅れのコマンド、無関係なパラメーター、またはアクセスできないリソースへのリンク。練習のショー:公式のドキュメントと手動チューニングを使用して、LLMを補助的な役割にします。たとえば、テンプレートコードや構成の適応を生成します。

なぜ原則がまだ機能しているのか – しかし、手動開発で

固体で乾燥した清潔から清潔からの困難は、LLMを介したコドヘゲエネーションに懸念していることを理解することが重要です。開発者がコードを手動で書き込むと、これらの原則は引き続き価値を示し続けます。つながりを低下させ、サポートを簡素化し、プロジェクトの読みやすさと柔軟性を高めます。

これは、人間の思考が一般化する傾向があるという事実によるものです。私たちはパターンを探しています。個々のエンティティに繰り返し論理を持ち込み、パターンを作成します。おそらく、この動作には進化的なルーツがあります。情報の量を減らすと、認知リソースが節約されます。

LLMは異なる行動をとる:彼らはデータの量から負荷を経験せず、貯蓄を努力しません。それどころか、複雑な抽象化を構築および維持するよりも、それらが重複した断片化された情報を使用して作業するのが簡単です。そのため、繰り返し構造と最小限のアーキテクチャの重大度を備えた、カプセル化なしでコードに対処する方が簡単です。

結論

大規模な言語モデルは、特に初期段階で、または補助コードを作成する際に、開発において有用なツールです。ただし、アプローチを適応させることが重要です。アーキテクチャを簡素化し、抽象化を制限し、複雑な依存関係を避け、SDKを構成する際にそれらに依存しないようにすることが重要です。

固体、乾燥、きれいの原則はまだ関連していますが、人の手に最善の効果をもたらします。 LLMで作業する場合、手動で簡単に完成できる信頼できる理解可能なコードを取得できる簡素化された実用的なスタイルを使用することが合理的です。そして、LLMが忘れる場所 – コードの複製は彼が覚えておくのに役立ちます。

Surreal Engine C++ を WebAssembly に移植

この投稿では、Surreal Engine ゲーム エンジンを WebAssembly に移植する方法について説明します。

シュール エンジン – Unreal Engine 1 の機能のほとんどを実装するゲーム エンジン、このエンジン上の有名なゲーム -アンリアル トーナメント 99、アンリアル、デウス エクス、アンダイング。これは、主にシングルスレッド実行環境で動作する古典的なエンジンを指します。

当初、私は、妥当な時間内に完了できないプロジェクトに挑戦して、Twitch フォロワーに私でも実行できないプロジェクトがあることを示すことを考えていました。最初のストリーム中に、Emscripten を使用して Surreal Engine C++ を WebAssembly に移植するタスクが実行可能であることに突然気づきました。

Surreal Engine Emscripten Demo

1 か月後には、WebAssembly でフォークとエンジンのアセンブリをデモンストレーションできるようになります。
https://demensdeum.com/demos/SurrealEngine/

オリジナルと同様に、コントロールはキーボードの矢印を使用して実行されます。次に、これをモバイル コントロール (タチ) に適応させ、Unreal Championship 99 レンダリングの正しいライティングやその他のグラフィック機能を追加する予定です。

どこから始めればよいですか?

最初に言いたいのは、Emscripten を使用すると、どのプロジェクトでも C++ から WebAssembly に移植できるということです。唯一の問題は、機能がどの程度完成するかということです。 Surreal Engine の場合は、ライブラリ ポートがすでに Emscripten で利用できるプロジェクトを選択してください。エンジンは SDL 2、OpenAL – ライブラリを使用します。どちらも Emscripten に移植されています。ただし、Vulkan はグラフィックス API として使用されており、現在 HTML5 では利用できません。WebGPU の実装作業が進行中ですが、これもドラフト段階にあり、Vulkan から WebGPU へのさらなる移植がどれほど簡単になるかも不明です。完全に標準化された後。したがって、Surreal Engine 用に独自の基本的な OpenGL-ES / WebGL レンダリングを作成する必要がありました。

プロジェクトのビルド

Surreal Engine でシステムを構築 – CMake も移植を簡素化します。 Emscripten はネイティブ ビルダーを提供します。えむけ、えむけ

Surreal Engine の移植は、Death-Mask と呼ばれる、WebGL/OpenGL ES および C++ で作成した最新ゲームのコードに基づいていました。そのため、開発ははるかに簡単で、必要なビルド フラグとコード サンプルがすべて揃っていました。

CMakeLists.txt の最も重要なポイントの 1 つは Emscripten のビルド フラグです。以下はプロジェクト ファイルの例です。


-s MAX_WEBGL_VERSION=2 \

-s EXCEPTION_DEBUG \

-fexceptions \

--preload-file UnrealTournament/ \

--preload-file SurrealEngine.pk3 \

--bind \

--use-preload-plugins \

-Wall \

-Wextra \

-Werror=return-type \

-s USE_SDL=2 \

-s ASSERTIONS=1 \

-w \

-g4 \

-s DISABLE_EXCEPTION_CATCHING=0 \

-O3 \

--no-heap-copy \

-s ALLOW_MEMORY_GROWTH=1 \

-s EXIT_RUNTIME=1")

ビルド スクリプト自体:


emmake make -j 16

cp SurrealEngine.data /srv/http/SurrealEngine/SurrealEngine.data

cp SurrealEngine.js /srv/http/SurrealEngine/SurrealEngine.js

cp SurrealEngine.wasm /srv/http/SurrealEngine/SurrealEngine.wasm

cp ../buildScripts/Emscripten/index.html /srv/http/SurrealEngine/index.html

cp ../buildScripts/Emscripten/background.png /srv/http/SurrealEngine/background.png

次に、インデックスを準備します。 .html 、プロジェクト ファイル システム プリローダーが含まれています。 Web にアップロードするために、Unreal Championship Demo バージョン 338 を使用しました。CMake ファイルからわかるように、解凍されたゲーム フォルダーがビルド ディレクトリに追加され、Emscripten のプリロード ファイルとしてリンクされました。

メインコードの変更

その後、ゲームのゲーム ループを変更する必要がありました。無限ループを実行することはできません。これによりブラウザがフリーズします。代わりに emscripten_set_main_loop を使用する必要があります。この機能については 2017 年のメモ「< a href="https://demensdeum.com /blog/ru/2017/03/29/porting-sdl-c-game-to-html5-emscripten/" rel="noopener" target="_blank">SDL C++ ゲームを HTML5 (Emscripten) に移植」
while ループを終了するコードを if に変更し、ゲーム ループを含むゲーム エンジンのメイン クラスをグローバル スコープに表示し、グローバル オブジェクトからゲーム ループ ステップを呼び出すグローバル関数を作成します。 :


#include <emscripten.h>

Engine *EMSCRIPTEN_GLOBAL_GAME_ENGINE = nullptr;

void emscripten_game_loop_step() {

	EMSCRIPTEN_GLOBAL_GAME_ENGINE->Run();

}

#endif

この後、アプリケーションにバックグラウンド スレッドがないことを確認する必要があります。存在する場合は、シングル スレッド実行用に書き直すか、Emscripten の phtread ライブラリを使用する準備をします。
Surreal Engine のバックグラウンド スレッドは音楽の再生に使用されます。データは、現在のトラック、音楽を再生する必要性、またはその不在に関するメイン エンジン スレッドから取得され、バックグラウンド スレッドはミューテックス経由で新しい状態を受け取り、新しい音楽の再生を開始します。 、または一時停止します。バックグラウンド ストリームは、再生中に音楽をバッファリングするためにも使用されます。
pthread を使用して Emscripten 用の Surreal Engine を構築しようとした試みは失敗しました。SDL2 ポートと OpenAL ポートは pthread サポートなしで構築されており、音楽のために再構築したくなかったためです。そこで、BGM ストリームの機能をループを使用したシングルスレッド実行に移行しました。 C++ コードから pthread 呼び出しを削除することで、バッファリングと音楽再生をメイン スレッドに移動し、遅延が発生しないように、バッファを数秒増やしました。

次に、グラフィックとサウンドの具体的な実装について説明します。

Vulkan はサポートされていません!

はい、Vulkan は HTML5 ではサポートされていませんが、すべてのマーケティング パンフレットでは Vulkan の主な利点としてクロスプラットフォームおよび広範なプラットフォームのサポートが紹介されています。このため、簡素化された OpenGL タイプの基本的なグラフィックス レンダラーを独自に作成する必要がありました。 ES はモバイル デバイスで使用され、最新の OpenGL のファッショナブルな機能が含まれていない場合もありますが、WebGL に非常によく移植されており、まさに Emscripten が実装しているものです。基本的なタイル レンダリング、最も単純な GUI 表示用の bsp レンダリング、およびモデルとマップのレンダリングの作成は 2 週間で完了しました。おそらくこれがこのプロジェクトで最も困難な部分でした。 Surreal Engine レンダリングの全機能を実装するには、まだ多くの作業が必要です。そのため、読者からのコードやプル リクエストの形での支援を歓迎します。

OpenAL がサポートされています!

幸運なことに、Surreal Engine はオーディオ出力に OpenAL を使用しています。 OpenAL で簡単な Hello World を記述し、Emscripten を使用して WebAssembly で組み立てたので、すべてがいかに単純であるかが明らかになり、サウンドの移植に着手しました。
数時間のデバッグ後、Emscripten の OpenAL 実装にはいくつかのバグがあることが明らかになりました。たとえば、モノラル チャネル数の読み取りを初期化するときに、メソッドが無限の数を返し、無限サイズのベクトルを初期化しようとした後、C++例外vector::length_errorでクラッシュします。
>
モノラル チャンネルの数を 2048 にハードコーディングすることで、この問題を回避することができました。


		alcGetIntegerv(alDevice, ALC_STEREO_SOURCES, 1, &stereoSources);



#if __EMSCRIPTEN__

		monoSources = 2048; // for some reason Emscripten's OpenAL gives infinite monoSources count, bug?

#endif



ネットワークはありますか?

Surreal Engine は現在オンライン プレイをサポートしていません。ボットでのプレイはサポートされていますが、これらのボット用の AI を作成する人が必要です。理論的には、Websocket を使用して WebAssembly/Emscripten にネットワーク ゲームを実装できます。

結論

結論として、Emscripten ポートがあるライブラリの使用と、WebAssembly 用に C++ でゲームを実装した過去の経験のおかげで、Surreal Engine の移植は非常にスムーズに完了したと言いたいと思います。エムスクリプテンで。以下は、このトピックに関する知識源とリポジトリへのリンクです。
マ、マ、マ、モンスターを倒せ!

また、できれば WebGL/OpenGL ES レンダリング コードでプロジェクトを支援したい場合は、Telegram で私に連絡してください。
https://t.me/demenscave

リンク

https://demensdeum.com/demos/Sur​​realEngine/
https://github.com/demensdeum/SurrealEngine-Emscripten

https://github.com/dpjudas/SurrealEngine

Flash Forever – Interceptor 2021

Recently, it turned out that Adobe Flash works quite stably under Wine. During a 4-hour stream, I made the game Interceptor 2021, which is a sequel to the game Interceptor 2020, written for the ZX Spectrum.

For those who are not in the know – the Flash technology provided interactivity on the web from 2000 to around 2015. Its shutdown was prompted by an open letter from Steve Jobs, in which he wrote that Flash should be consigned to history because it lagged on the iPhone. Since then, JS has become even more sluggish than Flash, and Flash itself has been wrapped in JS, making it possible to run it on anything thanks to the Ruffle player.

You can play it here:
https://demensdeum.com/demos/Interceptor2021

Video:
https://www.youtube.com/watch?v=-3b5PkBvHQk

Source code:
https://github.com/demensdeum/Interceptor-2021

CRUDリポジトリ

このノートでは、よく知られている古典的な CRUD パターン、Swift 言語での実装の基本原則について説明します。 Swift は、Windows、Linux、macOS、iOS、Android で利用できるオープンなクロスプラットフォーム言語です。

データ ストレージとアプリケーション ロジックを抽象化するためのソリューションは数多くあります。そのようなソリューションの 1 つが CRUD アプローチです。これは C– の頭字語です。作成、R -読み取り、U –最新情報、D–削除
します。通常、この原則はデータベースへのインターフェイスの実装を通じて実装され、要素は id などの一意の識別子を使用して操作されます。 CRUD – 文字ごとにインターフェイスが作成されます。作成(オブジェクト、ID)、読み取り(ID)、更新(オブジェクト、ID)、削除(オブジェクト、ID)。
オブジェクトの内部に ID が含まれている場合、オブジェクト全体がフィールド – とともに渡されるため、メソッド (Create、Update、Delete) から id 引数を省略できます。 ID。しかし、– についてはID によってデータベースからオブジェクトを取得したいため、読み取りには ID が必要です。

名前はすべて架空のものです

仮想の AssistantAI アプリケーションが無料の EtherRelm データベース SDK を使用して作成され、統合は簡単で、API は非常に便利で、その結果、アプリケーションが市場にリリースされたと想像してみましょう。
突然、SDK 開発者の EtherRelm は有料化を決定し、アプリケーション ユーザーあたりの価格を年間 100 ドルに設定しました。
何?はい! AssistantAI の開発者は、すでに 100 万人のアクティブ ユーザーを抱えているため、今何をすべきでしょうか。 1億ドル支払う?
代わりに、プラットフォーム固有の RootData データベースへのアプリケーションの転送を評価する決定が行われます。プログラマーによると、そのような転送には約 6 か月かかりますが、これにはアプリケーションの新機能の実装は考慮されていません。いくつか考えた結果、アプリケーションを市場から削除し、BueMS データベースが組み込まれた別の無料のクロスプラットフォーム フレームワークに書き直すことが決定されました。これにより、有料データベースの問題が解決され、他のプラットフォームでの開発が簡素化されます。
1 年後、アプリケーションは BueMS で書き直されましたが、突然フレームワーク開発者が有料化を決定しました。チームは同じ罠に 2 度陥ったことが判明しました。2 度目から抜け出せるかどうかはまったく別の話です。

抽象化が助けになります

開発者がアプリケーション内でインターフェイスの抽象化を使用していれば、これらの問題は回避できたはずです。 OOPの3つの柱へ–ポリモーフィズム、カプセル化、継承、少し前に別の – が追加されました。抽象化
します。データ抽象化により、ビジネス上の問題を解決するために使用される特定の実装を実装するのに十分な正確さを保ちながら、アイデアやモデルを最小限の詳細で一般的な用語で記述することができます。
アプリケーションロジックが依存しないようにデータベース操作を抽象化するにはどうすればよいでしょうか?私たちは CRUD アプローチを使用しています!

簡略化された UML CRUD 図は次のようになります。

架空の EtherRelm データベースの例:

実際の SQLite データベースの例:

すでにお気づきのとおり、データベースを切り替えると、アプリケーションがやり取りする CRUD インターフェイスのみが変更され、変更されません。 CRUD は GoF パターンの実装です。アダプターなので、これを使用して、アプリケーション インターフェイスをあらゆるデータベースに適合させ、互換性のないインターフェイスを結合します。
言葉は空です、コードを見せてください
プログラミング言語で抽象化を実装するには、インターフェイス/プロトコル/抽象クラスが使用されます。これらはすべて同じ程度の現象ですが、インタビュー中にそれらの違いを挙げるように求められることがありますが、個人的にはこれはあまり意味がないと考えています。使用の唯一の目的はデータ抽象化を実装することです。それ以外の場合は、インタビュー対象者の記憶をテストすることです。
CRUD は多くの場合、リポジトリ パターンのフレームワーク内で実装されますが、リポジトリは CRUD インターフェイスを実装する場合と実装しない場合があり、すべて開発者の創意工夫に依存します。

UserDefaults データベースを直接操作する、Book 構造リポジトリの非常に典型的な Swift コードを考えてみましょう。


struct Book: Codable {
	let title: String
	let author: String
}

class BookRepository {
	func save(book: Book) {
    		let record = try! JSONEncoder().encode(book)
    		UserDefaults.standard.set(record, forKey: book.title)
	}
    
	func get(bookWithTitle title: String) -> Book? {
    		guard let data = UserDefaults.standard.data(forKey: title) else { return nil }
    		let book = try! JSONDecoder().decode(Book.self, from: data)
    		return book
	}
    
	func delete(book: Book) {
    		UserDefaults.standard.removeObject(forKey: book.title)
	}
}

let book = Book(title: "Fear and Loathing in COBOL", author: "Sir Edsger ZX Spectrum")
let repository = BookRepository()
repository.save(book: book)
print(repository.get(bookWithTitle: book.title)!)
repository.delete(book: book)
guard repository.get(bookWithTitle: book.title) == nil else {
	print("Error: can't delete Book from repository!")
	exit(1)
}

上記のコードは単純に見えますが、DRY (Do notrepeat Yourself) 原則の違反の数とコードの一貫性を数えてみましょう。
UserDefaults データベースへの接続
JSON エンコーダーおよびデコーダーとの接続 – #8211; JSONエンコーダ、JSONデコーダ
Book 構造に接続されていますが、データベースに保存する構造ごとにリポジトリ クラスを作成しないように抽象リポジトリが必要です (DRY 違反)

この種の CRUD リポジトリ コードはよく見かけます。使用することはできますが、コードの高度な結合と重複により、時間の経過とともにそのサポートが非常に複雑になるという事実につながります。これは、別のデータベースに切り替えようとする場合、またはアプリケーションで作成されたすべてのリポジトリでデータベースを操作する内部ロジックを変更する場合に特に顕著になります。
コードを複製する代わりに、カップリングを高く保ちます。 CRUD リポジトリのプロトコルを作成して、データベース インターフェイスとアプリケーション ビジネス ロジックを抽象化し、DRY を尊重し、低結合を実装しましょう。

    typealias Item = Codable
    typealias ItemIdentifier = String
    
    func create<T: CRUDRepository.Item>(id: CRUDRepository.ItemIdentifier, item: T) async throws
    func read<T: CRUDRepository.Item>(id: CRUDRepository.ItemIdentifier) async throws -> T
    func update<T: CRUDRepository.Item>(id: CRUDRepository.ItemIdentifier, item: T) async throws
    func delete(id: CRUDRepository.ItemIdentifier) async throws
}

CRUDRepository プロトコルは、特定の CRUD リポジトリをさらに実装するためのインターフェイスと関連するデータ型を記述します。

次に、UserDefaults データベースの特定の実装を作成します。

    private typealias RecordIdentifier = String
    
    let tableName: String
    let dataTransformer: DataTransformer
    
    init(
   	 tableName: String = "",
   	 dataTransformer: DataTransformer = JSONDataTransformer()
    ) {
   	 self.tableName = tableName
   	 self.dataTransformer = dataTransformer
    }
    
    private func key(id: CRUDRepository.ItemIdentifier) -> RecordIdentifier {
   	 "database_\(tableName)_item_\(id)"
    }
   	 
    private func isExists(id: CRUDRepository.ItemIdentifier) async throws -> Bool {
   	 UserDefaults.standard.data(forKey: key(id: id)) != nil
    }
    
    func create<T: CRUDRepository.Item>(id: CRUDRepository.ItemIdentifier, item: T) async throws {
   	 let data = try await dataTransformer.encode(item)
   	 UserDefaults.standard.set(data, forKey: key(id: id))
   	 UserDefaults.standard.synchronize()
    }
    
    func read<T: CRUDRepository.Item>(id: CRUDRepository.ItemIdentifier) async throws -> T {
   	 guard let data = UserDefaults.standard.data(forKey: key(id: id)) else {
   		 throw CRUDRepositoryError.recordNotFound(id: id)
   	 }
   	 let item: T = try await dataTransformer.decode(data: data)
   	 return item
    }
    
    func update<T: CRUDRepository.Item>(id: CRUDRepository.ItemIdentifier, item: T) async throws {
   	 guard try await isExists(id: id) else {
   		 throw CRUDRepositoryError.recordNotFound(id: id)
   	 }
   	 let data = try await dataTransformer.encode(item)
   	 UserDefaults.standard.set(data, forKey: key(id: id))
   	 UserDefaults.standard.synchronize()
    }
    
    func delete(id: CRUDRepository.ItemIdentifier) async throws {
   	 guard try await isExists(id: id) else {
   		 throw CRUDRepositoryError.recordNotFound(id: id)
   	 }
   	 UserDefaults.standard.removeObject(forKey: key(id: id))
   	 UserDefaults.standard.synchronize()
    }
}

コードは長く見えますが、疎結合を含む CRUD リポジトリの完全な具体的な実装が含まれています。詳細は以下で説明します。
コードの自己文書化のために typealias が追加されました。
弱い結合と強い結合
について。特定の構造 (struct) からの切り離しは、汎用 T を使用して実装されます。これには、Codable プロトコルを実装する必要があります。 Codable では、TopLevelEncoder および TopLevelDecoder プロトコルを実装するクラス (JSONEncoder や JSONDecoder など) を使用して構造を変換できます。基本型 (Int、String、Float など) を使用する場合、構造を変換するために追加のコードを記述する必要はありません。 p>

特定のエンコーダとデコーダからの分離は、DataTransformer プロトコルの抽象化を使用して行われます。

	func encode<T: Encodable>(_ object: T) async throws -> Data
	func decode<T: Decodable>(data: Data) async throws -> T
}

データ トランスフォーマーの実装を使用して、エンコーダーとデコーダーのインターフェイスの抽象化を実装し、さまざまなタイプのデータ形式で確実に動作するように疎結合を実装しました。

以下は、特定の DataTransformer、つまり JSON のコードです。

	func encode<T>(_ object: T) async throws -> Data where T : Encodable {
    		let data = try JSONEncoder().encode(object)
    		return data
	}
    
	func decode<T>(data: Data) async throws -> T where T : Decodable {
    		let item: T = try JSONDecoder().decode(T.self, from: data)
    		return item
	}
}

それは可能でしたか?

何が変わりましたか?今後は、Codable プロトコルを実装する構造を操作できるように特定のリポジトリを初期化するだけで十分です。そのため、コードを複製する必要がなく、アプリケーションの疎結合を実装できます。

特定のリポジトリを使用したクライアント CRUD の例。UserDefaults はデータベース、JSON データ形式、クライアント構造であり、配列の書き込みと読み取りの例でもあります。


print("One item access example")

do {
	let clientRecordIdentifier = "client"
	let clientOne = Client(name: "Chill Client")
	let repository = UserDefaultsRepository(
    	tableName: "Clients Database",
    	dataTransformer: JSONDataTransformer()
	)
	try await repository.create(id: clientRecordIdentifier, item: clientOne)
	var clientRecord: Client = try await repository.read(id: clientRecordIdentifier)
	print("Client Name: \(clientRecord.name)")
	clientRecord.name = "Busy Client"
	try await repository.update(id: clientRecordIdentifier, item: clientRecord)
	let updatedClient: Client = try await repository.read(id: clientRecordIdentifier)
	print("Updated Client Name: \(updatedClient.name)")
	try await repository.delete(id: clientRecordIdentifier)
	let removedClientRecord: Client = try await repository.read(id: clientRecordIdentifier)
	print(removedClientRecord)
}
catch {
	print(error.localizedDescription)
}

print("Array access example")

let clientArrayRecordIdentifier = "clientArray"
let clientOne = Client(name: "Chill Client")
let repository = UserDefaultsRepository(
	tableName: "Clients Database",
	dataTransformer: JSONDataTransformer()
)
let array = [clientOne]
try await repository.create(id: clientArrayRecordIdentifier, item: array)
let savedArray: [Client] = try await repository.read(id: clientArrayRecordIdentifier)
print(savedArray.first!)

最初の CRUD チェック中に例外処理が実装されており、リモート アイテムの読み取りは利用できなくなります。

データベースの切り替え

次に、現在のコードを別のデータベースに転送する方法を説明します。たとえば、ChatGPT が生成した SQLite リポジトリ コードを取り上げます。


class SQLiteRepository: CRUDRepository {
    private typealias RecordIdentifier = String
    
    let tableName: String
    let dataTransformer: DataTransformer
    private var db: OpaquePointer?

    init(
   	 tableName: String,
   	 dataTransformer: DataTransformer = JSONDataTransformer()
    ) {
   	 self.tableName = tableName
   	 self.dataTransformer = dataTransformer
   	 self.db = openDatabase()
   	 createTableIfNeeded()
    }
    
    private func openDatabase() -> OpaquePointer? {
   	 var db: OpaquePointer? = nil
   	 let fileURL = try! FileManager.default
   		 .url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
   		 .appendingPathComponent("\(tableName).sqlite")
   	 if sqlite3_open(fileURL.path, &db) != SQLITE_OK {
   		 print("error opening database")
   		 return nil
   	 }
   	 return db
    }
    
    private func createTableIfNeeded() {
   	 let createTableString = """
   	 CREATE TABLE IF NOT EXISTS \(tableName) (
   	 id TEXT PRIMARY KEY NOT NULL,
   	 data BLOB NOT NULL
   	 );
   	 """
   	 var createTableStatement: OpaquePointer? = nil
   	 if sqlite3_prepare_v2(db, createTableString, -1, &createTableStatement, nil) == SQLITE_OK {
   		 if sqlite3_step(createTableStatement) == SQLITE_DONE {
       		 print("\(tableName) table created.")
   		 } else {
       		 print("\(tableName) table could not be created.")
   		 }
   	 } else {
   		 print("CREATE TABLE statement could not be prepared.")
   	 }
   	 sqlite3_finalize(createTableStatement)
    }
    
    private func isExists(id: CRUDRepository.ItemIdentifier) async throws -> Bool {
   	 let queryStatementString = "SELECT data FROM \(tableName) WHERE id = ?;"
   	 var queryStatement: OpaquePointer? = nil
   	 if sqlite3_prepare_v2(db, queryStatementString, -1, &queryStatement, nil) == SQLITE_OK {
   		 sqlite3_bind_text(queryStatement, 1, id, -1, nil)
   		 if sqlite3_step(queryStatement) == SQLITE_ROW {
       		 sqlite3_finalize(queryStatement)
       		 return true
   		 } else {
       		 sqlite3_finalize(queryStatement)
       		 return false
   		 }
   	 } else {
   		 print("SELECT statement could not be prepared.")
   		 throw CRUDRepositoryError.databaseError
   	 }
    }
    
    func create<T: CRUDRepository.Item>(id: CRUDRepository.ItemIdentifier, item: T) async throws {
   	 let insertStatementString = "INSERT INTO \(tableName) (id, data) VALUES (?, ?);"
   	 var insertStatement: OpaquePointer? = nil
   	 if sqlite3_prepare_v2(db, insertStatementString, -1, &insertStatement, nil) == SQLITE_OK {
   		 let data = try await dataTransformer.encode(item)
   		 sqlite3_bind_text(insertStatement, 1, id, -1, nil)
   		 sqlite3_bind_blob(insertStatement, 2, (data as NSData).bytes, Int32(data.count), nil)
   		 if sqlite3_step(insertStatement) == SQLITE_DONE {
       		 print("Successfully inserted row.")
   		 } else {
       		 print("Could not insert row.")
       		 throw CRUDRepositoryError.databaseError
   		 }
   	 } else {
   		 print("INSERT statement could not be prepared.")
   		 throw CRUDRepositoryError.databaseError
   	 }
   	 sqlite3_finalize(insertStatement)
    }
    
    func read<T: CRUDRepository.Item>(id: CRUDRepository.ItemIdentifier) async throws -> T {
   	 let queryStatementString = "SELECT data FROM \(tableName) WHERE id = ?;"
   	 var queryStatement: OpaquePointer? = nil
   	 var item: T?
   	 if sqlite3_prepare_v2(db, queryStatementString, -1, &queryStatement, nil) == SQLITE_OK {
   		 sqlite3_bind_text(queryStatement, 1, id, -1, nil)
   		 if sqlite3_step(queryStatement) == SQLITE_ROW {
       		 let queryResultCol1 = sqlite3_column_blob(queryStatement, 0)
       		 let queryResultCol1Length = sqlite3_column_bytes(queryStatement, 0)
       		 let data = Data(bytes: queryResultCol1, count: Int(queryResultCol1Length))
       		 item = try await dataTransformer.decode(data: data)
   		 } else {
       		 throw CRUDRepositoryError.recordNotFound(id: id)
   		 }
   	 } else {
   		 print("SELECT statement could not be prepared")
   		 throw CRUDRepositoryError.databaseError
   	 }
   	 sqlite3_finalize(queryStatement)
   	 return item!
    }
    
    func update<T: CRUDRepository.Item>(id: CRUDRepository.ItemIdentifier, item: T) async throws {
   	 guard try await isExists(id: id) else {
   		 throw CRUDRepositoryError.recordNotFound(id: id)
   	 }
   	 let updateStatementString = "UPDATE \(tableName) SET data = ? WHERE id = ?;"
   	 var updateStatement: OpaquePointer? = nil
   	 if sqlite3_prepare_v2(db, updateStatementString, -1, &updateStatement, nil) == SQLITE_OK {
   		 let data = try await dataTransformer.encode(item)
   		 sqlite3_bind_blob(updateStatement, 1, (data as NSData).bytes, Int32(data.count), nil)
   		 sqlite3_bind_text(updateStatement, 2, id, -1, nil)
   		 if sqlite3_step(updateStatement) == SQLITE_DONE {
       		 print("Successfully updated row.")
   		 } else {
       		 print("Could not update row.")
       		 throw CRUDRepositoryError.databaseError
   		 }
   	 } else {
   		 print("UPDATE statement could not be prepared.")
   		 throw CRUDRepositoryError.databaseError
   	 }
   	 sqlite3_finalize(updateStatement)
    }
    
    func delete(id: CRUDRepository.ItemIdentifier) async throws {
   	 guard try await isExists(id: id) else {
   		 throw CRUDRepositoryError.recordNotFound(id: id)
   	 }
   	 let deleteStatementString = "DELETE FROM \(tableName) WHERE id = ?;"
   	 var deleteStatement: OpaquePointer? = nil
   	 if sqlite3_prepare_v2(db, deleteStatementString, -1, &deleteStatement, nil) == SQLITE_OK {
   		 sqlite3_bind_text(deleteStatement, 1, id, -1, nil)
   		 if sqlite3_step(deleteStatement) == SQLITE_DONE {
       		 print("Successfully deleted row.")
   		 } else {
       		 print("Could not delete row.")
       		 throw CRUDRepositoryError.databaseError
   		 }
   	 } else {
   		 print("DELETE statement could not be prepared.")
   		 throw CRUDRepositoryError.databaseError
   	 }
   	 sqlite3_finalize(deleteStatement)
    }
}

または、同じく ChatGPT によって生成された、ファイル システムのリポジトリの CRUD コード:


class FileSystemRepository: CRUDRepository {
	private typealias RecordIdentifier = String
    
	let directoryName: String
	let dataTransformer: DataTransformer
	private let fileManager = FileManager.default
	private var directoryURL: URL
    
	init(
    	directoryName: String = "Database",
    	dataTransformer: DataTransformer = JSONDataTransformer()
	) {
    	self.directoryName = directoryName
    	self.dataTransformer = dataTransformer
   	 
    	let paths = fileManager.urls(for: .documentDirectory, in: .userDomainMask)
    	directoryURL = paths.first!.appendingPathComponent(directoryName)
   	 
    	if !fileManager.fileExists(atPath: directoryURL.path) {
        	try? fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil)
    	}
	}
    
	private func fileURL(id: CRUDRepository.ItemIdentifier) -> URL {
    	return directoryURL.appendingPathComponent("item_\(id).json")
	}
    
	private func isExists(id: CRUDRepository.ItemIdentifier) async throws -> Bool {
    	return fileManager.fileExists(atPath: fileURL(id: id).path)
	}
    
	func create<T: CRUDRepository.Item>(id: CRUDRepository.ItemIdentifier, item: T) async throws {
    	let data = try await dataTransformer.encode(item)
    	let url = fileURL(id: id)
    	try data.write(to: url)
	}
    
	func read<T: CRUDRepository.Item>(id: CRUDRepository.ItemIdentifier) async throws -> T {
    	let url = fileURL(id: id)
    	guard let data = fileManager.contents(atPath: url.path) else {
        	throw CRUDRepositoryError.recordNotFound(id: id)
    	}
    	let item: T = try await dataTransformer.decode(data: data)
    	return item
	}
    
	func update<T: CRUDRepository.Item>(id: CRUDRepository.ItemIdentifier, item: T) async throws {
    	guard try await isExists(id: id) else {
        	throw CRUDRepositoryError.recordNotFound(id: id)
    	}
    	let data = try await dataTransformer.encode(item)
    	let url = fileURL(id: id)
    	try data.write(to: url)
	}
    
	func delete(id: CRUDRepository.ItemIdentifier) async throws {
    	guard try await isExists(id: id) else {
        	throw CRUDRepositoryError.recordNotFound(id: id)
    	}
    	let url = fileURL(id: id)
    	try fileManager.removeItem(at: url)
	}
}

クライアント コード内のリポジトリを置き換えます。


print("One item access example")

do {
	let clientRecordIdentifier = "client"
	let clientOne = Client(name: "Chill Client")
	let repository = FileSystemRepository(
    	directoryName: "Clients Database",
    	dataTransformer: JSONDataTransformer()
	)
	try await repository.create(id: clientRecordIdentifier, item: clientOne)
	var clientRecord: Client = try await repository.read(id: clientRecordIdentifier)
	print("Client Name: \(clientRecord.name)")
	clientRecord.name = "Busy Client"
	try await repository.update(id: clientRecordIdentifier, item: clientRecord)
	let updatedClient: Client = try await repository.read(id: clientRecordIdentifier)
	print("Updated Client Name: \(updatedClient.name)")
	try await repository.delete(id: clientRecordIdentifier)
	let removedClientRecord: Client = try await repository.read(id: clientRecordIdentifier)
	print(removedClientRecord)
}
catch {
	print(error.localizedDescription)
}

print("Array access example")

let clientArrayRecordIdentifier = "clientArray"
let clientOne = Client(name: "Chill Client")
let repository = FileSystemRepository(
	directoryName: "Clients Database",
	dataTransformer: JSONDataTransformer()
)
let array = [clientOne]
try await repository.create(id: clientArrayRecordIdentifier, item: array)
let savedArray: [Client] = try await repository.read(id: clientArrayRecordIdentifier)
print(savedArray.first!)

UserDefaultsRepository の初期化は、適切な引数を備えた FileSystemRepository に置き換えられました。
クライアント コードの 2 番目のバージョンを実行すると、ドキュメント フォルダーに「Clients Database」ディレクトリが表示されます。このディレクトリには、1 つのクライアント構造を持つ JSON でシリアル化された配列のファイルが含まれています。

データ保存形式の切り替え

次に、ChatGPT に XML のエンコーダーとデコーダーを生成するよう依頼しましょう。

	let formatExtension = "xml"
    
	func encode<T: Encodable>(_ item: T) async throws -> Data {
    	let encoder = PropertyListEncoder()
    	encoder.outputFormat = .xml
    	return try encoder.encode(item)
	}
    
	func decode<T: Decodable>(data: Data) async throws -> T {
    	let decoder = PropertyListDecoder()
    	return try decoder.decode(T.self, from: data)
	}
}

Swift の組み込み型のおかげで、ニューラル ネットワークのタスクが初歩的になります。

クライアント コードで JSON を XML に置き換えます。


print("One item access example")

do {
	let clientRecordIdentifier = "client"
	let clientOne = Client(name: "Chill Client")
	let repository = FileSystemRepository(
    	directoryName: "Clients Database",
    	dataTransformer: XMLDataTransformer()
	)
	try await repository.create(id: clientRecordIdentifier, item: clientOne)
	var clientRecord: Client = try await repository.read(id: clientRecordIdentifier)
	print("Client Name: \(clientRecord.name)")
	clientRecord.name = "Busy Client"
	try await repository.update(id: clientRecordIdentifier, item: clientRecord)
	let updatedClient: Client = try await repository.read(id: clientRecordIdentifier)
	print("Updated Client Name: \(updatedClient.name)")
	try await repository.delete(id: clientRecordIdentifier)
	let removedClientRecord: Client = try await repository.read(id: clientRecordIdentifier)
	print(removedClientRecord)
}
catch {
	print(error.localizedDescription)
}

print("Array access example")

let clientArrayRecordIdentifier = "clientArray"
let clientOne = Client(name: "Chill Client")
let repository = FileSystemRepository(
	directoryName: "Clients Database",
	dataTransformer: XMLDataTransformer()
)
let array = [clientOne]
try await repository.create(id: clientArrayRecordIdentifier, item: array)
let savedArray: [Client] = try await repository.read(id: clientArrayRecordIdentifier)
print(savedArray.first!)

クライアント コードは 1 つの式 JSONDataTransformer -> のみに変更されました。 XMLDataTransformer

合計

CRUD リポジトリは、アプリケーション アーキテクチャ コンポーネントの疎結合を実装するために使用できる設計パターンの 1 つです。もう 1 つの解決策 – ORM (Object-Relational Mapping) を使用すると、ORM は構造をデータベースに完全にマッピングし、モデルによる変更をデータベース上に表示 (マッピング (!)) するアプローチを使用します。
しかし、それはまったく別の話です。

Swift 用の CRUD リポジトリの完全な実装は、次の場所から入手できます。
https://gitlab.com/demensdeum/crud-example

ところで、Swift は macOS 以外でも長い間サポートされてきましたが、この記事のコードはすべて Arch Linux 上で作成され、テストされました。

ソース

https://developer.apple.com/documentation/combine/topleveldecoder
https://developer.apple.com/documentation/combine/toplevelencoder
https://en.wikipedia.org/wiki/Create,_read,_update_and_delete

dd 入出力エラー

Linux で dd を使用して通常のディスクをコピーするときに入出力エラーが発生した場合はどうすればよいですか?

シトゥエヴィナは非常に悲しいですが、解決可能です。おそらく、使用、書き込み、読み取りができなくなった不良ブロックを含む故障したディスクを扱っていると考えられます。

そのようなディスクは必ず S.M.A.R.T. を使用してチェックしてください。ディスク エラーが表示される可能性が高くなります。私の場合はこれに当てはまりました。不良ブロックの数が非常に多かったので、古いハードドライブに別れを告げて、新しい SSD に交換する必要がありました。

問題は、このディスクには、作業に必要なライセンスされたソフトウェアを備えた完全に動作するシステムが含まれていることです。 partimage を使用してデータをすばやくコピーしようとしましたが、ユーティリティがディスクの 3 分の 1 だけをコピーし、セグメンテーション違反かその他の面白い Sishny/Sipplusplus ジョークで終了することに突然気づきました。

次に、ddを使用してデータをコピーしようとしましたが、ddがpartimageとほぼ同じ場所に到達し、入出力エラーが発生することがわかりました。同時に、conv=noerr、skip などのあらゆる種類の面白いフラグはまったく役に立ちませんでした。

しかし、ddrescue という GNU ユーティリティを使用すると、データは問題なく別のディスクにコピーされました。

После этого мои волосы стали шелковистыми, вернулась жена, дети и собака перестала кусать диван.

Большим плюсом ddrescue является наличие встроенного прогрессбара, поэтому не приходится костылять какие-то ухищрения навроде pv и всяких не особо красивых флажков dd. Также ddrescure показывает количество попыток прочитать данные; еще на вики написано что утилита обладает каким-то сверх алгоритмом для считывания поврежденных данных, оставим это на проверку людям которые любят ковыряться в исходниках, мы же не из этих да?

https://ru.wikipedia.org/wiki/Ddrescue
https://www.gnu.org/software/ddrescue/ddrescue_ru.html