Felhantering med C++/WinRT

I det här avsnittet beskrivs strategier för att hantera fel vid programmering med C++/WinRT. Mer allmän information och bakgrund finns i Fel och undantagshantering (modern C++).

Undvik att hantera och utlösa undantag

Vi rekommenderar att du fortsätter att skriva undantagssäker kod, men att du föredrar att undvika att fånga och utlösa undantag när det är möjligt. Om det inte finns någon hanterare för ett undantag genererar Windows automatiskt en felrapport (inklusive en minidump av kraschen), vilket hjälper dig att spåra var problemet finns.

Kasta inte ett undantag som du räknar med att fånga. Och använd inte undantag för förväntade fel. Utlöser endast ett undantag när ett oväntat körningsfel inträffar och hanterar allt annat med fel-/resultatkoder – direkt och nära källan till felet. På så sätt, när ett undantag utlöses, vet du att orsaken antingen är en bugg i koden eller ett exceptionellt feltillstånd i systemet.

Överväg scenariot med att komma åt Windows-registret. Om appen inte kan läsa ett värde från registret är det förväntat och du bör hantera det korrekt. Utlöser inte ett undantag. returnerar snarare ett bool eller enum -värde som anger det, och kanske varför, värdet inte lästes. Om du inte kan skriva ett värde till registret, å andra sidan, är det troligt att det finns ett större problem än vad du kan hantera på ett förnuftigt sätt i ditt program. I ett sånt fall vill du inte att programmet ska fortsätta, så ett undantag som resulterar i en felrapport är det snabbaste sättet att hindra programmet från att orsaka skada.

Ett annat exempel kan vara att hämta en miniatyrbild från ett anrop till StorageFile.GetThumbnailAsync och sedan skicka miniatyrbilden till BitmapSource.SetSourceAsync. Om den sekvensen av anrop får dig att skicka nullptr till SetSourceAsync (bildfilen kan inte läsas. Filnamnstillägget kanske får det att se ut som om det innehåller bilddata, men det gör det faktiskt inte), orsakar du ett ogiltigt pekarfel. Om du upptäcker ett sådant fall i koden ska du, i stället för att fånga och hantera det som ett undantagsfall, kontrollera om nullptr returneras från GetThumbnailAsync.

Att utlösa undantag tenderar att vara långsammare än att använda felkoder. Om du bara utlöser ett undantag när ett allvarligt fel inträffar kommer du aldrig att betala prestandapriset om allt går bra.

Men en mer sannolik prestandapåverkan handlar om omkostnaderna vid körning för att säkerställa att lämpliga destruktorer anropas om det osannolika skulle inträffa att ett undantag utlöses. Kostnaden för denna garanti uppstår oavsett om ett undantag faktiskt kastas eller inte. Därför bör du se till att kompilatorn har en bra uppfattning om vilka funktioner som potentiellt kan utlösa undantag. Om kompilatorn kan bevisa att det inte finns några undantag från vissa funktioner (specifikationen noexcept ) kan den optimera koden som genereras.

Fånga undantag

Ett feltillstånd som uppstår på Windows Runtime ABI-lagret returneras i form av ett HRESULT-värde. Men du behöver inte hantera HRESULT i koden. C++/WinRT-projektionskoden som genereras för ett API på den förbrukande sidan identifierar ett fel i HRESULT-koden på ABI-lagret och konverterar koden till ett winrt::hresult_error undantag som du kan fånga och hantera. Om du vill hantera HRESULT använder du typen winrt::hresult .

Om användaren till exempel råkar ta bort en bild från bildbiblioteket medan programmet itererar över samlingen, utlöser projektionen ett undantag. Och det här är ett fall där du måste fånga och hantera det undantaget. Här är ett kodexempel som visar det här fallet.

#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.
        }
    }
}

Använd samma mönster i en korutin när du anropar en funktion som märkts med co_await. Ett annat exempel på den här HRESULT-till-undantagskonverteringen är att när ett komponent-API returnerar E_OUTOFMEMORY, orsakar det att en std::bad_alloc genereras.

Föredra winrt::hresult_error::code när du bara vill kontrollera en HRESULT-kod. Funktionen winrt::hresult_error::to_abi konverterar, å andra sidan, till ett COM-felobjekt och lägger tillstånd i COM:s trådlokala lagring.

Utlösa undantag

Det kommer att finnas fall där du bestämmer dig för att om anropet till en viss funktion misslyckas kan programmet inte återställas (du kommer inte längre att kunna lita på att det fungerar förutsägbart). Kodexemplet nedan använder ett winrt::handle-värde som ett omslag kring det HANDLE som returneras av CreateEvent. Den skickar sedan handtaget (skapar ett bool värde från det) till funktionsmallen winrt::check_bool . winrt::check_bool fungerar med ett bool, eller med ett värde som kan konverteras till false (ett felvillkor) eller true (ett lyckat villkor).

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

Om värdet som du skickar till winrt::check_bool är falskt sker följande sekvens med åtgärder.

Eftersom Windows API:er rapporterar körningsfel med hjälp av olika typer av return-value finns det förutom winrt::check_bool en handfull andra användbara hjälpfunktioner för att kontrollera värden och utlösa undantag.

  • winrt::check_hresult. Kontrollerar om HRESULT-koden representerar ett fel och i så fall anropar winrt::throw_hresult.
  • winrt::check_nt. Kontrollerar om en kod representerar ett fel och anropar i så fall winrt::throw_hresult.
  • winrt::check_pointer. Kontrollerar om en pekare är null och i så fall anropar winrt::throw_last_error.
  • winrt::check_win32. Kontrollerar om en kod representerar ett fel och anropar i så fall winrt::throw_hresult.

Du kan använda dessa hjälpfunktioner för vanliga returkodstyper, eller så kan du svara på eventuella feltillstånd och anropa antingen winrt::throw_last_error eller winrt::throw_hresult.

Utlösa undantag vid redigering av ett API

Alla Windows Runtime Application Binary Interface-gränser (eller ABI-gränser) måste vara noexcept – vilket innebär att undantag aldrig får passera dessa gränser. När du skapar ett API bör du alltid markera ABI-gränsen med nyckelordet C++ noexcept . noexcept har ett specifikt beteende i C++. Om ett C++-undantag når en noexcept gräns misslyckas processen snabbt med std::terminate. Det beteendet är vanligtvis önskvärt, eftersom ett ohanterat undantag nästan alltid innebär okänt tillstånd i processen.

Eftersom undantag inte får korsa ABI-gränsen returneras ett felvillkor som uppstår i en implementering över ABI-lagret i form av en HRESULT-felkod. När du redigerar ett API med C++/WinRT genereras kod som du kan använda för att konvertera eventuella undantag som du genererar i implementeringen till en HRESULT. Funktionen winrt::to_hresult används i den genererade koden i ett mönster som detta.

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 hanterar undantag som härletts från std::exception och winrt::hresult_error och dess härledda typer. I implementeringen bör du föredra winrt::hresult_error, eller en härledd typ, så att användarna av api:et får omfattande felinformation. std::exception (som mappar till E_FAIL) stöds om undantag uppstår från din användning av standardmallbiblioteket.

Felsökningsbarhet med noexcept

Som vi nämnde ovan misslyckas ett C++-undantag som träffar en noexcept gräns snabbt med std::terminate. Det är inte idealiskt för felsökning, eftersom std::terminate ofta förlorar mycket eller hela felet eller undantagskontexten som genereras, särskilt när coroutines är inblandade.

Det här avsnittet handlar alltså om det fall där din ABI-metod (som du har kommenterat korrekt med noexcept) använder co_await för att anropa asynkron C++/WinRT-projektionskod. Vi rekommenderar att du omger anropen till C++/WinRT-projektionskoden med en winrt::fire_and_forget. Genom att göra det får ett ohanterat undantag en lämplig plats där det kan registreras korrekt som ett lagrat undantag, vilket avsevärt ökar felsökningsbarheten.

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 har en inbyggd unhandled_exception metodhjälp, som anropar winrt::terminate, som i sin tur anropar RoFailFastWithErrorContext. Detta garanterar att all kontext (lagrade undantag, felkod, felmeddelande, stackspårning och så vidare) bevaras antingen för livefelsökning eller för en postmortem-dumpfil. För enkelhetens skull kan du dela in fire-and-forget-delen i en separat funktion som returnerar en winrt::fire_and_forget och sedan anropa den.

Synkron kod

I vissa fall anropar din ABI-metod (som du återigen har kommenterat korrekt med noexcept) endast synkron kod. Med andra ord använder den aldrig co_await, vare sig för att anropa en asynkron Windows Runtime-metod eller för att växla mellan förgrunds- och bakgrundstrådar. I det fallet fungerar tekniken fire_and_forget fortfarande, men det är inte effektivt. I stället kan du göra något liknande.

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

Misslyckas snabbt

Koden i föregående avsnitt misslyckas fortfarande snabbt. Som det är skrivet hanterar koden inga undantag. Eventuella ohanterade undantag resulterar i programavslut.

Men den formen är överlägsen, eftersom den säkerställer felsökningsbarhet. I sällsynta fall kanske du vill try/catch, och hantera vissa undantag. Men det bör vara sällsynt eftersom vi, som det här avsnittet förklarar, avråder från att använda undantag som en flödeskontrollmekanism för villkor som du förväntar dig.

Kom ihåg att det är en dålig idé att låta ett ohanterat undantag slippa igenom en naken noexcept-kontext. Under detta villkor kommer C++-körningen att std::avsluta processen och därmed förlora all lagrad undantagsinformation som C++/WinRT noggrant registrerade.

Assertions

För interna antaganden i din applikation finns asserter. Föredra static_assert för kompileringstidsverifiering, där det är möjligt. För körningsvillkor använder du WINRT_ASSERT med ett booleskt uttryck. WINRT_ASSERT är en makrodefinition och expanderas till _ASSERTE.

WINRT_ASSERT(pos < size());

WINRT_ASSERT kompileras bort i releaseversioner; i en felsökningsversion stoppar felsökaren programmet på den kodrad där assertsatsen finns.

Du bör inte använda undantag i destruktorer. Så åtminstone i felsökningsversioner kan du kontrollera resultatet av att anropa en funktion från en destruktor med WINRT_VERIFY (med ett booleskt uttryck) och WINRT_VERIFY_ (med ett förväntat resultat och ett booleskt uttryck).

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

Viktiga API:er