- 特定のプラットフォームや廃止状態のために実際の機能を提供できなくても、既存アプリを壊さないためには 文書化された API 契約 の範囲内で「何もしない」動作を設計する必要がある
- Xbox のように印刷インフラがない環境では、
NotSupportedException よりも プリンターが 0 台 という成功応答のほうが、PC 基準で作られたアプリには安全である
- このような方式は inert API と呼ばれ、API の表面と仕様は維持しつつ、実際には有用な処理を行わない設計である
- 廃止されるウィジェット API で
CreateWidget が成功しながら nullptr を返すと、呼び出し側が矛盾した状態に陥るため、文書化された ERROR_CANCELLED の失敗経路のほうが一貫している
- 正しい inert API は機能だけを消し、既存コードがすでに想定している失敗経路を使って 例外・不正なハンドル・クラッシュ を減らす
機能がなくても API 契約は残る
- API を 何もしないように しなければならない場合があるが、その無動作も既存コードと文書化された動作に合っていなければならない
- 誤って設計された無動作は、例外、矛盾した戻り値、無効な状態を生み、アプリのクラッシュや不要なユーザーエラーにつながる可能性がある
Xbox で印刷 API を無力化する方法
- Windows には広範な 印刷インフラ があるが、Xbox にはそのインフラがない
- Xbox でアプリが印刷を試みたとき、印刷関数が
NotSupportedException を投げると問題が大きくなりうる
- Xbox にインストールされたアプリは、主に PC でテストされていた可能性が高い
- PC では印刷が常に可能なので、未処理の例外がアプリのクラッシュにつながることがある
- 例外を捕捉しても、ユーザーには「サポートチームに incident code を伝えてください」のようなエラーメッセージが表示されうる
- より良い設計は、印刷関数は成功しつつ インストールされたプリンターがない ことを報告することである
- アプリはプリンター選択 UI を表示し、空の一覧を見せる
- ユーザーはプリンターがないことを見て、印刷要求を取り消せる
- プリンターのインストールを支援しようとするアプリには、インストール関数が即座に戻り、ユーザーが操作を取り消した ことを意味する結果コードを返せる
inert API の条件
- このような「何もしない」動作を inert と呼ぶ
- inert API は API の表面がそのまま存在し、仕様どおりに動作するが、実際には何の有用な処理もしない
- 重要なのは、文書と一貫し、既存コードに問題を起こす可能性が最も低い方法で何もしないことだ
- 印刷 API には、印刷自体が可能かどうかを確認する関数を追加することもできる
- アプリはこの関数を使って、印刷をまったくサポートしないシステムでは Print ボタンを隠せる
- 単に印刷可能だと仮定するアプリでも、「プリンターがなく、インストールを試みても効果のないシステム」のように動作する
廃止されるウィジェット API で避けるべき設計
- 廃止対象の API には、ウィジェット ハンドルを作る関数、ウィジェット ハンドルを受け取る関数、ウィジェット ハンドルを閉じる関数がある
- 初期案は
CreateWidget が S_OK を返しながら *widget = nullptr を設定する方式だった
- 呼び出しは成功したのに、有効なハンドルを受け取れない状態になる
- 実際のテストコードの中には、戻り値ではなくハンドルが
null かどうかで成功を判定するコードもあった
EnableWidget が「不正なハンドル」を返す設計も呼び出し側を混乱させる
- アプリは
CreateWidget の成功後に受け取ったハンドルを EnableWidget に渡す
- API はそのハンドルが無効だと応答する
- 呼び出し側は「ウィジェットを要求して受け取ったのに、もう一度見せるとウィジェットではないと言われる」という矛盾した状態を経験する
文書化された失敗経路へ修正した設計
- 既存の文書には、ユーザーがウィジェットの作成を取り消したことを意味する
ERROR_CANCELLED の戻り値がある
- アプリはすでに外部条件によりウィジェットが作成されない可能性を扱う必要があるため、ウィジェット作成が常に ユーザーの取り消し として失敗するようにできる
- 修正後の設計の流れは単純である
CreateWidget は *widget = nullptr のままにし、HRESULT_FROM_WIN32(ERROR_CANCELLED) を返す
- ウィジェット作成は常に失敗するので、有効なウィジェット ハンドルは存在しない
GetWidgetAliases、EnableWidget、Close は、ハンドルが無効であることを示す E_HANDLE を返す
- この方式ではウィジェットがないため、ダミーの alias を作る必要もない
- 有効なウィジェットが存在しないので、アプリが正常に alias を要求できるケースもない
デスクトップ API を別の環境へ持ち込むときの基準
- デスクトップでは印刷 API は常に存在しており、「プリンター一覧を取得する」関数は例外を投げないよう文書化されている
- この API を Xbox に持ち込む際、既存のデスクトップアプリを動き続けさせるには、「Xbox にはプリンターがない」という事実を正直に返す inert な動作が適切である
- 「プリンターは何台あるか?」という問いに例外を投げるのは、文書化された API 契約に合っていない
- 既存 API を廃止するときも同じ原則が当てはまる
- 有用な処理は行わなくても、API の動作は契約と一貫していなければならない
- 既存コードがすでに処理できる戻り値と状態を使うことが重要である
2件のコメント
Lobste.rs の意見
冗談はさておき、このような過度に防御的なプログラミングやユーザー体験には同意できない。これではソフトウェアが理由も分からないまま役目を果たさず、なぜそうなっているのか知る方法もなくなる。アプリはエラーを捕捉して、できる限りユーザーフレンドリーなメッセージを作るべきで、そうでなければ元のエラーメッセージでもユーザーに見せるべきだ。バックグラウンド作業ならエラーログがあるべきだ
この記事がアプリ開発者ではなく API 開発者の視点で書かれている点は認める。だから API のエラーを文書化し、呼び出し側で対処できるエラーメッセージを提供すべきだ
また、アクセス権がないという理由で UI からボタンを隠すのも嫌いだ。スペースが許すならボタンは表示したまま無効化し、ユーザーがマウスオーバーしたときにどうすれば有効化できるかを知らせるメッセージを出すほうがよいと思う
全体としては正確さを要求するほうがよい。ただし既存ユーザーが 10 億人いるなら、できるだけ壊さないのは非常に賢明で、ユーザーの立場からは単に動くので、システム全体としての実際の価値も生まれる。結局のところ態度としては早く失敗せよ、ただし大規模には失敗させるなであるべきだ
これは API というよりABI 安定性に近い。Windows では 15 年前にビルドされたソフトウェアでも、できる限り新しい OS 上で動き続けなければならない。関数シグネチャを変えられないので、もはや意味をなさない API でも動き続けるようにするには、善意の嘘をつく必要がある
たとえば API は今でも Active Desktop が存在するふりをしている。代替案は古い既存ソフトウェアを大量に壊すことだからだ
その機能が消えたのか、その間に別の画面に埋もれてしまったのか分からなくなる
だがアプリがそうしないと、そのアプリを使う人たちは Windows を責める。アプリではなく Windows を責め、アプリがクラッシュした場合でさえ同じだ
だから Microsoft は回避策を作る。印刷ジョブがただ消えるようにしておくほうがはるかに簡単で、そうすればユーザーは少し考えてから「ああ、そうだ、プリンターがないんだ」と受け入れる
ifチェックを頻繁に入れるのを見かける。そういうのを見るたびにこの記事を思い出すそこでエージェントには null チェックを繰り返さず、無害な関数を使い、宣言時点でその値が決して null ではないことを一度だけ確認するよう指示した
Hacker Newsの意見
エラーを握りつぶすやり方として学んできたし、良くない慣行だと思う
Xboxで印刷できないという実際の問題を解決できないだけでなく、ソフトウェアがどれほど壊れているかを隠してしまい、バグ発見やテストをはるかに難しくする
Goの
panicは、ランタイムで乱用したり復旧したりするためのものではなく、テスト時点でプログラマーが間違ったことを大きなサイレンで知らせる道具なので良いシステム内のアクターが自分の欠陥やエラーを積極的に隠すと、原因を突き止めて直すことが指数関数的に難しくなる
自分が扱っているB2B、マイクロサービス、API中心のシステムでは、仕様外のリクエストはすばやく失敗させるのが正しい
しかしWindowsは互換性のために信じられないほど後方へ配慮してきており、SimCityのように壊れた有名ソフトウェアをメモリパッチまで当てて生かした例もある: https://arstechnica.com/gadgets/2022/10/windows-95-went-the-...
ユーザーがシステムコードを直せないデスクトップ環境では、多くの場合、正確性よりもエンドユーザー体験のほうが重要になる
BSODがなぜ起きたかは重要ではなく、悪いソフトウェアやハードウェアのせいでMicrosoftの評判が悪くなるのだとMicrosoftは学んだということだ
個人的には、不正な入力や条件ではすばやく失敗または停止する設計を好むが、この環境ではこうしたアプローチの価値も理解できる
重要なのはコンテキストであって、どちらか一方を教条的に適用することではない
たとえば原子炉や超高速取引の設計にChenのアプローチを持ち込むべきではないし、ゲームエンジンには優れたアプローチになり得る
ランタイムコンポーネントを「無害に非アクティブ」な状態にしておくかどうかは、実装が
panicするかどうかより前に来る設計上の判断だこの場合、「Xboxで印刷する」の意味を定義するのはMicrosoftの役目
エラーとして定義することもできるし、理論的には後でサポート可能だが今は実際の実行方法がない機能として定義することもできる
USBプリンターを挿し、GB PrinterをサポートしていたGameboyゲームの拡張ポートのような「ゲーム」が印刷方法を知っているなら、理論上Xboxでも印刷できる
Xboxゲームが印刷を試みることは必ずしも本質的なエラーではなく、エラーとして定義することもアプリケーションの移植性のようなトレードオフを伴う選択にすぎない
Xboxのように選択可能なプリンターがない選択画面を出すこともできるし、APIに印刷したと嘘をつかせることも、実際に印刷をサポートすることもできる
「Xboxで印刷」の意味をエラーとして定義して初めて、そのエラーが投げられず握りつぶされるかどうかが実装やデバッグの問題になる
可能な処理は2つある。マシンにプリンターが絶対にないのでエラーを出すか、マシンにプリンターが絶対にないので空のプリンター一覧を返すか
どちらも妥当だが、ユーザー空間を壊さないのは片方だけだ
「大きなサイレンを鳴らす」ことはテスターや開発者には良いが、エンドユーザーにはあまり良くない
エンドユーザーの立場では、エラーを握りつぶすほうがアプリが落ちるよりましだ
何が失敗したのか分からない。自分のコンピューターのソフトウェア、ハードウェア、EthernetケーブルやWi-Fi、スイッチ、ルーター、ケーブルモデム、家まで来ているインターネットケーブル、ISP、接続しようとしているリモートシステムのどれなのか、まったく改善されていない
否定的に反応されるのは予想できるが、こうした泥臭い作業こそが、Word ’97文書や30年前にMS-DOS向けにコンパイルされたゲームをクリックしたときに、期待どおり開ける理由だ
後方互換性は常にmessyで、不完全にやるか、まったくやらないかのどちらかだ
結局GOGを使うことになり、GOGも結局は環境を仮想化している
たとえばオリジナルのSim CityをWindows 11にインストールして成功する人はいないと思う
書式はインストールされているプリンタードライバーに依存していた
一方、35年前のLaTeXファイルはいまでもうまく動く
Wordは同じコンピューター、同じバージョンで開き直しても書式が変わらないとは保証できず、それでTeXを学ぶことになった
ゲームもMicrosoftがGFWLを終了したことで多数のゲームを壊した
オリジナルのDark SoulsはSteamで再リリースされるまでプレイできず、海賊版を使わざるを得なかった
「サポートされていません」というダイアログが表示され、dosboxをダウンロードすることはできるが、それは「正確に期待どおり」ではない
ほとんど毎回諦めるかGOGで買い直し、実質的には互換性問題を整理してくれた作業にお金を払ったようなものだ
実際には存在し得るかのように UI が示唆しているのに、今は存在しないと表示されるデバイス UI ほどもどかしいものはない
結局、時間をかけて初めて、それらのデバイスはサポートされておらず、その画面は誰かが作ったモックアップにすぎなかったのだと分かることになる
アプリが出力非対応の状況を想定していないのに出力が例外を投げると、そのアプリはおそらくそのまま落ちる
出力非対応を想定したアプリなら、明示的な API でまず出力対応の有無を確認し、対応していなければ出力 UI を表示すべきではない
この記事はそういうアプリではなく、事前確認しないアプリをどう扱うかについての話である
バグはアプリ側にあるが、良いプラットフォームは悪いアプリもただ落ちるに任せるのではなく、できるだけ動くようにしようとすべきだ
Microsoft の役割は、ソフトウェア提供者がプリンターを考慮していなかったからといってアプリが落ちないようにすることにある
アプリが印刷しようとしたときにユーザーにプリンターを選ばせ、空の一覧を見せる動作とは。Microsoft は 30 年たっても学べなかったようだ: https://www.reddit.com/r/hacking/comments/djvzd/windows_nt_l...
記事で提案されている具体的な方向、つまりユーザーが苦しむ前にコンポーネントが苦しむべきだという話は正しいが、フレーミングは非常に引っかかる
「API に何もしないようにさせるべき場合がある」「出力関数が
NotSupportedExceptionを投げるのは間違いだ」といった言い方は、決して一般的な助言ではないこれはひどいクライアントをサポートするためのハックであり、時には必要ではあるが、正常なものでも一般化できる助言でもない
こうした表現には、Microsoft 開発者としての苦痛を内面化した痕跡が表れている
クライアントが変わっていないのに API がそれを壊すなら、ひどいのはクライアントではなく API だ
広く採用され、後方互換性を真剣に考えるあらゆるプラットフォームでは、まったく正常で一般化可能なことだ
Windows が代表例だが、Linux カーネル ABI、glibc、Web プラットフォーム、Java などでも同じことが起きている
時間を巻き戻してクライアントを再コンパイルしたり書き直したりすることはできない
クライアントがすでに
NotSupportedExceptionをチェックしているならそれを返すべきだが、そうでなければ別のアプローチが必要だここは Windows だ
ユーザーが依存しているアプリケーションを壊してはいけない
「ひどい」あるいは誤動作するクライアントアプリがあるなら、期待どおりに動かすために必要な回避策、shim、互換性ハックをすべて使うべきだ
SimCity を動かすために特殊なメモリ管理コードを入れなければならないとしても同じである
あなたの顧客はそのクライアントに依存しており、あなたが変えたせいで止まればあなたを責めるだろう
ここでは皆が「何もしないこと」と「エラー処理」にこだわっているが、この記事はあらゆる問題を飲み込んで kumbaya を歌おうと言っているのではない、という説明がうまくできていないようだ
実際の文脈は、変更されないソフトウェアのためのエミュレーションと互換性であり、たいていの状況とはかなり違う
Microsoft 自身も両者の境界をしばしば曖昧にするのでさらに混乱するが、ここで言っているのは一般的なエラー処理ではない
エミュレーションとは本質的に嘘をつく行為である
Xbox には実際にはプリンターを接続する方法はなく、Linux 上で Windows ゲームを動かしており、偽の iPhone は実は AWS の ARM サーバーかもしれない
嘘をつくなら説得力を持たせなければならず、そうして初めて既存アプリケーションが動き続ける
Microsoft が既存の Windows アプリを Xbox にポーティングできると言うとき、Windows アプリには印刷できるという約束があったのだから、Xbox はそれが可能に見えるように嘘をつかなければならない
一方、プログラムが use-after-free をしているのに、バグがないふりをして黙って迂回するなら、それは正しい処理ではない
既存の use-after-free まで新しいプラットフォームでバグ単位で互換になると約束した人はいないからだ
その解釈が望む立場を支持するから仮定しただけである
実際、記事の内容は私が明日 Xbox 向けプログラムを書いて印刷しようとする場合にもそのまま適用される
この動作が古く保守されていないソフトウェアだけに該当すると区別する記述はない
このアプローチは好きでもあり嫌いでもある
本能的には、悪意ある遵守で問題に対処するのは好きではない
一方で、印刷が壊れていても、より多くのユーザーにより多くのソフトウェアをそのプラットフォーム上で実行させることが目的なら、良い判断だという点には完全に同意する
このコメントの流れは奇妙だった
Microsoft がすることなら何でも嫌う人が確かにいる
ブログの核心は、アプリ(UWP)を XBOX に持ってくるプロセスを摩擦なくすることだ
再コンパイルも、汚い
ifdefも、追加作業もなしにアプリが「そのまま動く」ようにし、その後で開発者がアプリを XBOX プラットフォームに合わせて調整すればよい重要なのは、開発者が何の努力もなくアプリを XBOX に持ってこられたという点であり、これはプラットフォームと開発者の双方にとって良いことだ
ユーザーにとっては、プリンター一覧が空なので印刷できないという明確なサインになる
何より、アプリが落ちたり「問題が発生しました。後でもう一度お試しください」のような最悪のエラーを表示したりしない
そのメッセージの方が優れたユーザー体験というわけではない
開発者がアプリを再コンパイルして、より説明的なメッセージを入れることはできるが、そうするとまた振り出しに戻る
たとえばエラーを握りつぶしているという非難は、核心ではまったくない
この記事を批判する側が見落としている核心は、ここには例外を投げるべき例外的な状況がないという点である
このデバイスにはプリンターが接続されておらず、この場合は定義上そうなのだから、アプリはその状況をきれいに処理すべきで、落ちてはいけない
ノートPCがプリンターにアクセスできないからといって、例外を投げることはないはずだ
設定でプリンターを追加したあと、インストール中に「Color Profiles」タブへ移動すると、タイミングが合ったときにgnome-control-centerが落ちる
よく見ると、利用可能なプリンターを列挙しようとしたものの、プリンターがあるべきことは分かっているが、まだ情報が存在しないため、そのまま落ちている
幸い、GNOMEがプリンターのインストールを終えるまで数秒待ってから設定を開き直せばよい
それでも、多様な背景を持つエンドユーザーがデスクトップをきちんと使えるようにする中核アプリなら、理想の世界では決して落ちてはいけない
境界条件を考えるのは難しく、境界条件の処理自体にもさらに別の境界条件があると想像するのはもっと難しい
正しいかどうかは別として、少なくとも筆者は「悪いアプリだからWaylandをサポートするように書き直すべきだ」よりは深く考えている
かつてブラウザーがHTMLコードに誤りがあっても、最善を尽くしてページを表示することは優れた戦略だと考えられていた
作者の意図をできる限り推測して先に進み、エラーは悪でありユーザーはエラーを望まない、という考え方だった
その経験から学んだと思っていたが、そうではないようだ
厳密な検証で置き換えようとしたXHTMLの試みは失敗し、代わりに成功したHTML5は、考え得るあらゆる不正なシーケンスをどう解釈すべきかを明示的に定義し、ブラウザーが少なくとも一貫して動作するようにした
初期のブラウザーが初めて見るタグに遭遇した途端にエラーを表示していたなら、新しいブラウザー機能は生まれた瞬間に窒息していただろう
HelloとWorld!を入力してみれば、私たちが得た知恵のおかげで、今ではエラーが出るのかどうか分かるそれでもなお正しくレンダリングされるなら、まだ学ぶべきことが多いという意味か、あるいは特定の種類のエラーは優雅に処理し、例外的な状況では落ちるというやり方にも意味があるということかもしれない
1999年にタイムスリップしても、それほど惜しいものはなさそうだ