Foutafhandeling met C++/WinRT

In dit onderwerp worden strategieën besproken voor het afhandelen van fouten bij het programmeren met C++/WinRT. Zie Fouten en uitzonderingsafhandeling (modern C++) voor meer algemene informatie en achtergrond.

Vermijd het vangen en genereren van uitzonderingen

We raden u aan om uitzonderingsveilige code te blijven schrijven, maar geef er waar mogelijk de voorkeur aan om het afvangen en gooien van uitzonderingen te vermijden. Als er geen handler is voor een uitzondering, genereert Windows automatisch een foutenrapport (inclusief een minidump van de crash), waarmee u kunt achterhalen waar het probleem zich bevindt.

Gooi geen uitzondering die u verwacht te vangen. En gebruik geen uitzonderingen voor verwachte fouten. Een uitzondering alleen genereren wanneer er een onverwachte runtimefout optreedt en alle andere fouten/resultaatcodes verwerken, rechtstreeks en dicht bij de bron van de fout. Op die manier weet u, wanneer er een uitzondering wordt opgegooid, dat de oorzaak ofwel een fout in uw code is, ofwel een uitzonderlijke fouttoestand in het systeem.

Overweeg het scenario voor toegang tot het Windows-register. Als uw app een waarde uit het register niet kan lezen, is dat te verwachten en moet u deze probleemloos afhandelen. Gooi geen uitzondering; retourneer eerder een bool of enum waarde die aangeeft dat en misschien waarom de waarde niet is gelezen. Als het daarentegen niet lukt een waarde naar het register weg te schrijven, duidt dat waarschijnlijk op een ernstiger probleem dan u in uw toepassing op een zinvolle manier kunt afhandelen. In een dergelijk geval wilt u niet dat uw toepassing doorgaat, dus een uitzondering die resulteert in een foutenrapport, is de snelste manier om te voorkomen dat uw toepassing schade veroorzaakt.

U kunt bijvoorbeeld een miniatuurafbeelding ophalen uit een aanroep naar StorageFile.GetThumbnailAsync en deze miniatuur vervolgens doorgeven aan BitmapSource.SetSourceAsync. Als deze reeks aanroepen ervoor zorgt dat u nullptr doorgeeft (het afbeeldingsbestand kan niet worden gelezen; misschien ziet de bestandsextensie eruit alsof deze afbeeldingsgegevens bevat, maar dat is eigenlijk niet), dan wordt er een ongeldige aanwijzer-uitzondering gegenereerd. Als u een dergelijke case in uw code ontdekt, in plaats van de case als uitzondering te vangen en te verwerken, controleert u in plaats daarvan of nullptr deze wordt geretourneerd vanuit GetThumbnailAsync.

Het genereren van uitzonderingen is meestal langzamer dan het gebruik van foutcodes. Als u alleen een uitzondering opgooit wanneer er een fatale fout optreedt, dan betaalt u daar nooit de prestatieprijs voor als alles goed verloopt.

Maar een waarschijnlijkere oorzaak van prestatieverlies is de runtime-overhead die gepaard gaat met het ervoor zorgen dat de juiste destructors worden aangeroepen in het onwaarschijnlijke geval dat er een uitzondering wordt opgegooid. De kosten van deze zekerheid komen of er al dan niet een uitzondering wordt gegenereerd. U moet er dus voor zorgen dat de compiler een goed idee heeft van welke functies mogelijk uitzonderingen kunnen genereren. Als de compiler kan bewijzen dat er geen uitzonderingen van bepaalde functies (de noexcept specificatie) zijn, kan deze de code optimaliseren die wordt gegenereerd.

Uitzonderingen vangen

Een foutvoorwaarde die zich voordoet bij de Windows Runtime ABI-laag wordt geretourneerd in de vorm van een HRESULT-waarde. U hoeft echter geen HRESULTs in uw code te verwerken. De C++/WinRT-projectiecode die wordt gegenereerd voor een API aan de verbruikende kant detecteert een HRESULT-foutcode op de ABI-laag en converteert de code naar een winrt::hresult_error uitzondering, die u kunt ondervangen en verwerken. Als u HRESULTs wilt verwerken, gebruikt u het type winrt::hresult .

Als de gebruiker bijvoorbeeld een afbeelding uit de afbeeldingenbibliotheek verwijdert terwijl uw toepassing deze verzameling herhaalt, genereert de projectie een uitzondering. En dit is een geval waarin u die uitzondering moet ondervangen en afhandelen. Hier volgt een codevoorbeeld waarin deze case wordt weergegeven.

#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.Storage.h>
#include <winrt/Microsoft.UI.Xaml.Media.Imaging.h>

using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Storage;
using namespace Microsoft::UI::Xaml::Media::Imaging;

IAsyncAction MakeThumbnailsAsync()
{
    auto imageFiles{ co_await KnownFolders::PicturesLibrary().GetFilesAsync() };

    for (StorageFile const& imageFile : imageFiles)
    {
        BitmapImage bitmapImage;
        try
        {
            auto thumbnail{ co_await imageFile.GetThumbnailAsync(FileProperties::ThumbnailMode::PicturesView) };
            if (thumbnail) bitmapImage.SetSource(thumbnail);
        }
        catch (winrt::hresult_error const& ex)
        {
            winrt::hresult hr = ex.code(); // HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND).
            winrt::hstring message = ex.message(); // The system cannot find the file specified.
        }
    }
}

Gebruik hetzelfde patroon in een coroutine bij het aanroepen van een co_await-ed-functie. Een ander voorbeeld van deze HRESULT-naar-uitzonderingsconversie is dat wanneer een component-API E_OUTOFMEMORY retourneert, een std::bad_alloc wordt gegenereerd.

Geef de voorkeur aan winrt::hresult_error::code wanneer u alleen een HRESULT-code bekijkt. De functie winrt::hresult_error::to_abi converteert naar een COM-foutobject en pusht de status naar de com-thread-lokale opslag.

Excepties gooien

Er zijn gevallen waarin u besluit dat uw aanroep naar een bepaalde functie mislukt, uw toepassing niet meer kan herstellen (u kunt er niet langer op vertrouwen om voorspelbaar te werken). In het onderstaande codevoorbeeld wordt een winrt::handle-waarde gebruikt als een wrapper rond de HANDLE die wordt geretourneerd door CreateEvent. Vervolgens wordt de handle, waarbij er een waarde van bool van wordt gemaakt, doorgegeven aan de functiesjabloon winrt::check_bool. winrt::check_bool werkt met een bool, of met een waarde die converteerbaar is naar false (een foutvoorwaarde) of true (een succesvoorwaarde).

winrt::handle h{ ::CreateEvent(nullptr, false, false, nullptr) };
winrt::check_bool(bool{ h });
winrt::check_bool(::SetEvent(h.get()));

Als de waarde die u doorgeeft aan winrt::check_bool onwaar is, vindt de volgende reeks acties plaats.

Omdat Windows API's runtimefouten melden met behulp van verschillende typen retourwaarden, zijn er naast winrt::check_bool een aantal andere nuttige helperfuncties voor het controleren van waarden en het genereren van uitzonderingen.

  • winrt::check_hresult. Hiermee wordt gecontroleerd of de HRESULT-code een fout vertegenwoordigt en, als dat het zo is, winrt::throw_hresult aanroept.
  • winrt::check_nt. Hiermee wordt gecontroleerd of een code een fout vertegenwoordigt en, als dat het zo is, winrt::throw_hresult aanroept.
  • winrt::check_pointer. Hiermee wordt gecontroleerd of een aanwijzer null is en, als dat het zo is, winrt::throw_last_error aanroept.
  • winrt::check_win32. Hiermee wordt gecontroleerd of een code een fout vertegenwoordigt en, als dat het zo is, winrt::throw_hresult aanroept.

U kunt deze helperfuncties gebruiken voor veelvoorkomende retourcodetypen of u kunt reageren op een foutvoorwaarde en winrt::throw_last_error of winrt::throw_hresult aanroepen.

Uitzonderingen genereren bij het ontwerpen van een API

Alle Windows Runtime Application Binary Interface-grenzen (of ABI-grenzen) moeten noexcept zijn, wat betekent dat uitzonderingen daar nooit moeten ontsnappen. Wanneer u een API maakt, moet u altijd de ABI-grens markeren met het C++ noexcept -trefwoord. noexcept heeft specifiek gedrag in C++. Als een C++-uitzondering een noexcept grens bereikt, mislukt het proces snel met std::terminate. Dat gedrag is over het algemeen wenselijk, omdat een onverwerkte uitzondering bijna altijd een onbekende status in het proces impliceert.

Aangezien uitzonderingen de ABI-grens niet moeten overschrijden, wordt een foutvoorwaarde die zich voordoet in een implementatie, geretourneerd over de ABI-laag in de vorm van een HRESULT-foutcode. Wanneer u een API schrijft met C++/WinRT, wordt er code voor u gegenereerd om elke uitzondering die u daadwerkelijk in uw implementatie genereert, om te zetten in een HRESULT. De functie winrt::to_hresult wordt gebruikt in die gegenereerde code in een patroon zoals dit.

HRESULT DoWork() noexcept
{
    try
    {
        // Shim through to your C++/WinRT implementation.
        return S_OK;
    }
    catch (...)
    {
        return winrt::to_hresult(); // Convert any exception to an HRESULT.
    }
}

winrt::to_hresult verwerkt uitzonderingen die zijn afgeleid van std::exception en winrt::hresult_error en de afgeleide typen. In uw implementatie moet u de voorkeur geven aan winrt::hresult_error of een afgeleid type, zodat gebruikers van uw API uitgebreide foutinformatie ontvangen. std::exception (die wordt toegewezen aan E_FAIL) wordt ondersteund in het geval er uitzonderingen optreden bij uw gebruik van de standaardsjabloonbibliotheek.

Debugbaarheid met noexcept

Zoals we hierboven hebben aangegeven, wordt een C++-uitzondering die de grens van noexcept bereikt onmiddellijk afgebroken met std::terminate. Dat is niet ideaal voor foutopsporing, omdat std::terminate vaak veel of alle fouten of de uitzonderingscontext verliest, met name wanneer coroutines betrokken zijn.

Deze sectie gaat over het geval waarin uw ABI-methode (die u correct hebt geannoteerd met noexcept) co_await gebruikt om asynchrone C++/WinRT-projectiecode aan te roepen. We raden u aan de aanroepen naar de C++/WinRT-projectiecode te verpakken in een winrt::fire_and_forget. Dit biedt een juiste plaats voor een niet-verwerkte uitzondering die correct wordt vastgelegd als een opgeslagen uitzondering, waardoor de foutopsporing aanzienlijk toeneemt.

HRESULT MyWinRTObject::MyABI_Method() noexcept
{
    winrt::com_ptr<Foo> foo{ get_a_foo() };

    [/*no captures*/](winrt::com_ptr<Foo> foo) -> winrt::fire_and_forget
    {
        co_await winrt::resume_background();

        foo->ABICall();

        AnotherMethodWithLotsOfProjectionCalls();
    }(foo);

    return S_OK;
}

winrt::fire_and_forget heeft een ingebouwde unhandled_exception methode-helper, die winrt::terminate aanroept, waardoor RoFailFastWithErrorContext op zijn beurt wordt aangeroepen. Dit garandeert dat elke context (opgeslagen uitzondering, foutcode, foutbericht, stack backtrace, enzovoort) behouden blijft voor live foutopsporing of voor een post-mortemdump. Voor het gemak kunt u het fire-and-forget-deel onderbrengen in een afzonderlijke functie die een winrt::fire_and_forget teruggeeft, en die functie vervolgens aanroepen.

synchrone code

In sommige gevallen roept uw ABI-methode (die u, opnieuw, correct hebt geannoteerd met noexcept) alleen synchrone code aan. Met andere woorden, het gebruikt co_awaitnooit, ofwel om een asynchrone Windows Runtime methode aan te roepen, of om te schakelen tussen voorgrond- en achtergrondthreads. In dat geval werkt de fire-and-forget-techniek nog steeds, maar die is niet efficiënt. In plaats daarvan kunt u zoiets doen.

HRESULT abi() noexcept try
{
    // ABI code goes here.
} catch (...) { winrt::terminate(); }

Snel mislukken

De code in de vorige sectie mislukt nog steeds snel. Zoals geschreven, verwerkt die code geen uitzonderingen. Elke niet-verwerkte uitzondering resulteert in programma-beëindiging.

Maar die vorm is beter, omdat die goed te debuggen is. In zeldzame gevallen wilt u misschien try/catch en bepaalde uitzonderingen afhandelen. Maar dat zou zeldzaam moeten zijn, omdat we, zoals in dit onderwerp wordt uitgelegd, het gebruik van uitzonderingen als een mechanisme voor besturingsstroom afraden voor situaties die u verwacht.

Onthoud dat het een slecht idee is om een niet-afgevangen uitzondering te laten ontsnappen uit een kale noexcept-context. Onder deze voorwaarde zal de C++-runtime std::het proces beëindigen , waardoor alle opgeslagen uitzonderingsgegevens verloren gaan die door C++/WinRT zorgvuldig zijn vastgelegd.

Assertions

Voor interne veronderstellingen in uw toepassing zijn er asserties. Geef de voorkeur aan static_assert voor compileertijdvalidatie, waar mogelijk. Gebruik voor runtimevoorwaarden WINRT_ASSERT met een Boole-expressie. WINRT_ASSERT is een macrodefinitie en wordt uitgebreid naar _ASSERTE.

WINRT_ASSERT(pos < size());

WINRT_ASSERT wordt weggelaten in release-builds; in een debug-build stopt het de applicatie in de debugger op de coderegel waar de assertie staat.

Je zou geen uitzonderingen moeten gebruiken in je destructoren. Dus kunt u, in elk geval in foutopsporingsbuilds, het resultaat van het aanroepen van een functie vanuit een destructor controleren met WINRT_VERIFY (met een Booleaanse expressie) en WINRT_VERIFY_ (met een verwacht resultaat en een Booleaanse expressie).

WINRT_VERIFY(::CloseHandle(value));
WINRT_VERIFY_(TRUE, ::CloseHandle(value));

Belangrijke API's