2 ポイント 投稿者 GN⁺ 2024-02-17 | 2件のコメント | WhatsAppで共有
  • 特定のプラットフォームや廃止状態のために実際の機能を提供できなくても、既存アプリを壊さないためには 文書化された 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 には、ウィジェット ハンドルを作る関数、ウィジェット ハンドルを受け取る関数、ウィジェット ハンドルを閉じる関数がある
  • 初期案は CreateWidgetS_OK を返しながら *widget = nullptr を設定する方式だった
    • 呼び出しは成功したのに、有効なハンドルを受け取れない状態になる
    • 実際のテストコードの中には、戻り値ではなくハンドルが null かどうかで成功を判定するコードもあった
  • EnableWidget が「不正なハンドル」を返す設計も呼び出し側を混乱させる
    • アプリは CreateWidget の成功後に受け取ったハンドルを EnableWidget に渡す
    • API はそのハンドルが無効だと応答する
    • 呼び出し側は「ウィジェットを要求して受け取ったのに、もう一度見せるとウィジェットではないと言われる」という矛盾した状態を経験する

文書化された失敗経路へ修正した設計

  • 既存の文書には、ユーザーがウィジェットの作成を取り消したことを意味する ERROR_CANCELLED の戻り値がある
  • アプリはすでに外部条件によりウィジェットが作成されない可能性を扱う必要があるため、ウィジェット作成が常に ユーザーの取り消し として失敗するようにできる
  • 修正後の設計の流れは単純である
    • CreateWidget*widget = nullptr のままにし、HRESULT_FROM_WIN32(ERROR_CANCELLED) を返す
    • ウィジェット作成は常に失敗するので、有効なウィジェット ハンドルは存在しない
    • GetWidgetAliasesEnableWidgetClose は、ハンドルが無効であることを示す E_HANDLE を返す
  • この方式ではウィジェットがないため、ダミーの alias を作る必要もない
    • 有効なウィジェットが存在しないので、アプリが正常に alias を要求できるケースもない

デスクトップ API を別の環境へ持ち込むときの基準

  • デスクトップでは印刷 API は常に存在しており、「プリンター一覧を取得する」関数は例外を投げないよう文書化されている
  • この API を Xbox に持ち込む際、既存のデスクトップアプリを動き続けさせるには、「Xbox にはプリンターがない」という事実を正直に返す inert な動作が適切である
  • 「プリンターは何台あるか?」という問いに例外を投げるのは、文書化された API 契約に合っていない
  • 既存 API を廃止するときも同じ原則が当てはまる
    • 有用な処理は行わなくても、API の動作は契約と一貫していなければならない
    • 既存コードがすでに処理できる戻り値と状態を使うことが重要である

2件のコメント

 
GN⁺ 2024-02-17
Lobste.rs の意見
  • ここで言っているのは、印刷機能が印刷が完全にサポートされているかのように一貫して動作する一方で、奇妙なことに実際に印刷するプリンターは決して存在しない、という意味で、これでいろいろ説明がつく
    冗談はさておき、このような過度に防御的なプログラミングやユーザー体験には同意できない。これではソフトウェアが理由も分からないまま役目を果たさず、なぜそうなっているのか知る方法もなくなる。アプリはエラーを捕捉して、できる限りユーザーフレンドリーなメッセージを作るべきで、そうでなければ元のエラーメッセージでもユーザーに見せるべきだ。バックグラウンド作業ならエラーログがあるべきだ
    この記事がアプリ開発者ではなく API 開発者の視点で書かれている点は認める。だから API のエラーを文書化し、呼び出し側で対処できるエラーメッセージを提供すべきだ
    また、アクセス権がないという理由で UI からボタンを隠すのも嫌いだ。スペースが許すならボタンは表示したまま無効化し、ユーザーがマウスオーバーしたときにどうすれば有効化できるかを知らせるメッセージを出すほうがよいと思う
    • この記事は単なる API 開発者ではなく、Windows API 開発者の視点だという点も考慮すべきだ。Windows API では、バージョンが変わっても互換性を壊してはならないというのが Microsoft の長年の立場だ
    • これは典型的な ポステルの法則 / 堅牢性原則 の議論だ。今では堅牢性原則が時代遅れで、HTML や適切なパーサーを書くのが難しい巨大なプロトコルやファイル形式のような怪物を生んだことを誰もが知っている
      全体としては正確さを要求するほうがよい。ただし既存ユーザーが 10 億人いるなら、できるだけ壊さないのは非常に賢明で、ユーザーの立場からは単に動くので、システム全体としての実際の価値も生まれる。結局のところ態度としては早く失敗せよ、ただし大規模には失敗させるなであるべきだ
    • この場合は、既存 API の 1 つが文書化されたエラーを持っていないため、そもそもエラー処理がない可能性が高い。既存ソフトウェアをすべて壊したくないなら、善意の嘘をつくしかない
      これは API というよりABI 安定性に近い。Windows では 15 年前にビルドされたソフトウェアでも、できる限り新しい OS 上で動き続けなければならない。関数シグネチャを変えられないので、もはや意味をなさない API でも動き続けるようにするには、善意の嘘をつく必要がある
      たとえば API は今でも Active Desktop が存在するふりをしている。代替案は古い既存ソフトウェアを大量に壊すことだからだ
    • 本当に同意する。何らかの理由で自分の画面には表示されないのに、横で説明している人やチュートリアルでは見えているボタンを探して右往左往するほど苛立つことはない
      その機能が消えたのか、その間に別の画面に埋もれてしまったのか分からなくなる
    • アプリがエラーを捕捉してユーザーに表示すべきなのはその通りだ。
      だがアプリがそうしないと、そのアプリを使う人たちは Windows を責める。アプリではなく Windows を責め、アプリがクラッシュした場合でさえ同じだ
      だから Microsoft は回避策を作る。印刷ジョブがただ消えるようにしておくほうがはるかに簡単で、そうすればユーザーは少し考えてから「ああ、そうだ、プリンターがないんだ」と受け入れる
  • プリンターのインストール関数が即座に戻り、結果コードとしてユーザーが操作をキャンセルしたを返せるなら、アプリケーションサポートの観点では、印刷 API が最初から例外を投げるよりも、はるかに扱いにくい望ましくない動作につながる可能性がほぼ確実に高い
  • 最近はAI 支援プログラミングをよくしているが、エージェントが null かどうかを確認する if チェックを頻繁に入れるのを見かける。そういうのを見るたびにこの記事を思い出す
    そこでエージェントには null チェックを繰り返さず、無害な関数を使い、宣言時点でその値が決して null ではないことを一度だけ確認するよう指示した
 
GN⁺ 2024-02-17
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で印刷」の意味をエラーとして定義して初めて、そのエラーが投げられず握りつぶされるかどうかが実装やデバッグの問題になる
    • これはエラーを握りつぶすことではなく、Linux流に言えばユーザー空間を壊さないことに近い
      可能な処理は2つある。マシンにプリンターが絶対にないのでエラーを出すか、マシンにプリンターが絶対にないので空のプリンター一覧を返すか
      どちらも妥当だが、ユーザー空間を壊さないのは片方だけだ
    • 記事を読むと、開発者体験よりもユーザー体験について話している
      「大きなサイレンを鳴らす」ことはテスターや開発者には良いが、エンドユーザーにはあまり良くない
      エンドユーザーの立場では、エラーを握りつぶすほうがアプリが落ちるよりましだ
    • インターネット接続が失敗したとき、何十年もただ失敗したというメッセージだけが出るのが腹立たしい
      何が失敗したのか分からない。自分のコンピューターのソフトウェア、ハードウェア、EthernetケーブルやWi-Fi、スイッチ、ルーター、ケーブルモデム、家まで来ているインターネットケーブル、ISP、接続しようとしているリモートシステムのどれなのか、まったく改善されていない
  • 否定的に反応されるのは予想できるが、こうした泥臭い作業こそが、Word ’97文書や30年前にMS-DOS向けにコンパイルされたゲームをクリックしたときに、期待どおり開ける理由だ
    後方互換性は常にmessyで、不完全にやるか、まったくやらないかのどちらかだ

    • Microsoftについてそういう決まり文句をよく聞くが、ゲームでは運が良くなかった
      結局GOGを使うことになり、GOGも結局は環境を仮想化している
      たとえばオリジナルのSim CityをWindows 11にインストールして成功する人はいないと思う
    • Word ’97で作成したファイルは、25年前でさえ別々の2台のコンピューターで正確に期待どおり開くことはなかった
      書式はインストールされているプリンタードライバーに依存していた
      一方、35年前のLaTeXファイルはいまでもうまく動く
    • 1997年のファイルでも運が良くなければならず、どのオフィスアプリで開いても単に間違って表示される可能性が高い
      Wordは同じコンピューター、同じバージョンで開き直しても書式が変わらないとは保証できず、それでTeXを学ぶことになった
      ゲームもMicrosoftがGFWLを終了したことで多数のゲームを壊した
      オリジナルのDark SoulsはSteamで再リリースされるまでプレイできず、海賊版を使わざるを得なかった
    • 最新の64ビットWindowsではMS-DOSソフトウェアは動作しない
      「サポートされていません」というダイアログが表示され、dosboxをダウンロードすることはできるが、それは「正確に期待どおり」ではない
    • これが本当ならよかったのだが、子どもの頃に保管していたゲームを動かすのにものすごく苦労した
      ほとんど毎回諦めるかGOGで買い直し、実質的には互換性問題を整理してくれた作業にお金を払ったようなものだ
  • 実際には存在し得るかのように UI が示唆しているのに、今は存在しないと表示されるデバイス UI ほどもどかしいものはない
    結局、時間をかけて初めて、それらのデバイスはサポートされておらず、その画面は誰かが作ったモックアップにすぎなかったのだと分かることになる

    • それでも、アプリが単に落ちるよりはまだましだと思う
      アプリが出力非対応の状況を想定していないのに出力が例外を投げると、そのアプリはおそらくそのまま落ちる
      出力非対応を想定したアプリなら、明示的な API でまず出力対応の有無を確認し、対応していなければ出力 UI を表示すべきではない
      この記事はそういうアプリではなく、事前確認しないアプリをどう扱うかについての話である
      バグはアプリ側にあるが、良いプラットフォームは悪いアプリもただ落ちるに任せるのではなく、できるだけ動くようにしようとすべきだ
    • ここでは Microsoft ではなく、ソフトウェア作者がプリンターの状況を扱わなかった責任の方が大きいかもしれない
      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 などでも同じことが起きている
    • 既存クライアントがすでに使っている API を実装しなければならない場合がある
      時間を巻き戻してクライアントを再コンパイルしたり書き直したりすることはできない
      クライアントがすでに NotSupportedException をチェックしているならそれを返すべきだが、そうでなければ別のアプローチが必要だ
    • Windows チームにいながらそんな態度を示したなら、私が管理者なら解雇していたかもしれない
      ここは 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 に持ってこられたという点であり、これはプラットフォームと開発者の双方にとって良いことだ
    ユーザーにとっては、プリンター一覧が空なので印刷できないという明確なサインになる
    何より、アプリが落ちたり「問題が発生しました。後でもう一度お試しください」のような最悪のエラーを表示したりしない
    そのメッセージの方が優れたユーザー体験というわけではない
    開発者がアプリを再コンパイルして、より説明的なメッセージを入れることはできるが、そうするとまた振り出しに戻る

    • このスレッドの 95% は記事を理解していないようだ
      たとえばエラーを握りつぶしているという非難は、核心ではまったくない
  • この記事を批判する側が見落としている核心は、ここには例外を投げるべき例外的な状況がないという点である
    このデバイスにはプリンターが接続されておらず、この場合は定義上そうなのだから、アプリはその状況をきれいに処理すべきで、落ちてはいけない
    ノートPCがプリンターにアクセスできないからといって、例外を投げることはないはずだ

    • 広く使われている非Microsoft製品がプリンターの可用性を誤って扱っている現代的な例が欲しいなら、gnome-control-centerがある
      設定でプリンターを追加したあと、インストール中に「Color Profiles」タブへ移動すると、タイミングが合ったときにgnome-control-centerが落ちる
      よく見ると、利用可能なプリンターを列挙しようとしたものの、プリンターがあるべきことは分かっているが、まだ情報が存在しないため、そのまま落ちている
      幸い、GNOMEがプリンターのインストールを終えるまで数秒待ってから設定を開き直せばよい
      それでも、多様な背景を持つエンドユーザーがデスクトップをきちんと使えるようにする中核アプリなら、理想の世界では決して落ちてはいけない
      境界条件を考えるのは難しく、境界条件の処理自体にもさらに別の境界条件があると想像するのはもっと難しい
      正しいかどうかは別として、少なくとも筆者は「悪いアプリだからWaylandをサポートするように書き直すべきだ」よりは深く考えている
  • かつてブラウザーがHTMLコードに誤りがあっても、最善を尽くしてページを表示することは優れた戦略だと考えられていた
    作者の意図をできる限り推測して先に進み、エラーは悪でありユーザーはエラーを望まない、という考え方だった
    その経験から学んだと思っていたが、そうではないようだ

    • ブラウザーは今でもその戦略に従っている
      厳密な検証で置き換えようとしたXHTMLの試みは失敗し、代わりに成功したHTML5は、考え得るあらゆる不正なシーケンスをどう解釈すべきかを明示的に定義し、ブラウザーが少なくとも一貫して動作するようにした
    • ブラウザーが入力に寛容だったおかげで、現代のWebが可能になった
      初期のブラウザーが初めて見るタグに遭遇した途端にエラーを表示していたなら、新しいブラウザー機能は生まれた瞬間に窒息していただろう
    • HelloWorld!を入力してみれば、私たちが得た知恵のおかげで、今ではエラーが出るのかどうか分かる
      それでもなお正しくレンダリングされるなら、まだ学ぶべきことが多いという意味か、あるいは特定の種類のエラーは優雅に処理し、例外的な状況では落ちるというやり方にも意味があるということかもしれない
    • 過去20年間のソフトウェアの進歩は、LLMを除けばあまり素晴らしいものではなく、LLMも検閲で台無しにされた
      1999年にタイムスリップしても、それほど惜しいものはなさそうだ
    • 私たちが何を学んだと思っていたのか気になる