winrt::implements 구조체 템플릿은 고유한 C++/WinRT 구현(런타임 클래스 및 활성화 팩토리)이 직접 또는 간접적으로 파생되는 기반이 됩니다.
이 항목에서는 C++/WinRT 2.0에서 winrt::implements 의 확장 지점에 대해 설명합니다. 검사 가능한 개체의 기본 동작을 사용자 지정하기 위해 구현 유형에서 이러한 확장 지점을 구현하도록 선택할 수 있습니다(
이러한 확장 지점을 사용하면 구현 유형의 소멸을 연기하고, 소멸 중에 안전하게 쿼리할 수 있으며, 프로젝션된 메서드의 진입과 종료에 연결할 수 있습니다. 이 항목에서는 이러한 기능에 대해 설명하고 사용 시기 및 방법에 대해 자세히 설명합니다.
지연된 소멸
직접 할당 진단
공용 소멸자를 갖는다는 것은 소멸을 지연시킬 수 있다는 이점이 있습니다. 이는 개체에 대한 최종 IUnknown::Release 호출을 감지하여, 그 개체의 소유권을 가져와 소멸을 무기한 연기할 수 있음을 의미합니다.
클래식 COM 개체는 본질적으로 참조 횟수가 계산됩니다. 참조 수는 IUnknown::AddRef 및 IUnknown::Release 함수를 통해 관리됩니다. 기존 Release 구현에서는 참조 수가 0에 도달하면 클래식 COM 개체의 C++ 소멸자가 호출됩니다.
uint32_t WINRT_CALL Release() noexcept
{
uint32_t const remaining{ subtract_reference() };
if (remaining == 0)
{
delete this;
}
return remaining;
}
delete this;
은 개체가 차지하는 메모리를 해제하기 전에 해당 개체의 소멸자를 호출합니다. 소멸자에서 흥미로운 작업을 수행할 필요가 없으면 충분히 잘 작동합니다.
using namespace winrt::Windows::Foundation;
...
struct Sample : implements<Sample, IStringable>
{
winrt::hstring ToString() const;
~Sample() noexcept
{
// Too late to do anything interesting.
}
};
흥미로운는 무엇을 의미합니까? 한 가지, 소멸자가 본질적으로 동기적입니다. 스레드를 전환할 수 없습니다. 아마도 다른 컨텍스트에서 일부 스레드별 리소스를 삭제하기 위해서일 수 있습니다. 특정 리소스를 해제하기 위해 필요할 수도 있는 다른 인터페이스에 대해 객체를 신뢰성 있게 쿼리할 수 없습니다. 목록은 계속됩니다. 소멸이 사소하지 않은 경우 보다 유연한 솔루션이 필요합니다. 여기서 C++/WinRT의 final_release 함수가 들어옵니다.
struct Sample : implements<Sample, IStringable>
{
winrt::hstring ToString() const;
static void final_release(std::unique_ptr<Sample> ptr) noexcept
{
// This is the first stop...
}
~Sample() noexcept
{
// ...And this happens only when *unique_ptr* finally deletes the object.
}
};
C++/WinRT 구현을 업데이트하여 개체의 참조 수가 0으로 전환될 때 final_release를 호출하도록 릴리스를 수정했습니다. 이 상태에서 개체는 더 이상 미해결 참조가 없다고 확신할 수 있고, 이제는 자체의 단독 소유권을 갖게 됩니다. 따라서 자체 소유권을 정적 final_release 함수로 이전할 수 있습니다.
즉, 이 객체는 공유 소유권을 지원하는 상태에서 독점 소유 상태로 전환되었습니다. std::unique_ptr은 개체에 대한 배타적인 소유권을 가지므로, 의미상 자연스럽게 개체를 삭제합니다. 따라서, std::unique_ptr가 범위를 벗어날 때(그 전에 다른 것으로 이동되지 않은 경우), 공용 소멸자가 필요합니다. 이것이 핵심입니다. std::unique_ptr 개체를 활성 상태로 유지하는 경우, 개체를 무기한으로 사용할 수 있습니다. 개체를 다른 곳으로 이동하는 방법에 대한 일러스트레이션은 다음과 같습니다.
struct Sample : implements<Sample, IStringable>
{
winrt::hstring ToString() const;
static void final_release(std::unique_ptr<Sample> ptr) noexcept
{
batch_cleanup.push_back(std::move(ptr));
}
};
이 코드는 batch_cleanup 컬렉션에 개체를 저장합니다. 그 중 하나는 앱 런타임의 향후 시점에 모든 개체를 정리하는 작업입니다.
일반적으로 std::unique_ptr가 소멸될 때 개체도 함께 소멸되지만, std::unique_ptr::reset를 호출하여 소멸을 앞당길 수 있습니다. 또는 std::unique_ptr를 어딘가에 저장하여 그 소멸을 연기할 수 있습니다.
아마도 더 실용적이면서도 강력하게, 당신은 final_release 함수를 코루틴으로 변경하여 한 곳에서 그 파기를 처리할 수 있으며, 필요할 때 스레드를 일시 중단하고 전환할 수 있습니다.
struct Sample : implements<Sample, IStringable>
{
winrt::hstring ToString() const;
static winrt::fire_and_forget final_release(std::unique_ptr<Sample> ptr) noexcept
{
co_await winrt::resume_background(); // Unwind the calling thread.
// Safely perform complex teardown here.
}
};
일시 중단 지점 때문에 원래 IUnknown::Release 함수 호출을 시작한 호출 스레드가 반환되며, 이로 인해 호출자에게 한때 보유했던 개체가 해당 인터페이스 포인터를 통해 더 이상 사용 불가능하다는 신호를 보냅니다. UI 프레임워크는 개체가 원래 개체를 만든 특정 UI 스레드에서 제거되도록 해야 하는 경우가 많습니다. 이 기능은 파괴가 객체 해제와 분리되어 있기에, 이와 같은 요구 사항을 쉽게 충족할 수 있습니다.
final_release 전달된 개체는 C++ 개체일 뿐입니다. 더 이상 COM 개체가 아닙니다. 예를 들어 개체에 대한 기존 COM 약한 참조는 더 이상 해결되지 않습니다.
소멸 중 안전한 쿼리
지연된 소멸의 개념을 기반으로 하는 것은 소멸 중에 인터페이스를 안전하게 쿼리하는 기능입니다.
클래식 COM은 두 가지 핵심 개념을 기반으로 합니다. 첫 번째는 참조 계산이고, 두 번째는 인터페이스를 쿼리하는 것입니다. AddRef 및 Release외에도 IUnknown 인터페이스는 QueryInterface를 제공합니다. 이 메서드는 XAML과 같은 특정 UI 프레임워크에서 구성 가능한 형식 시스템을 시뮬레이트할 때 XAML 계층 구조를 트래버스하는 데 많이 사용됩니다. 간단한 예제를 생각해 보세요.
struct MainPage : PageT<MainPage>
{
~MainPage()
{
DataContext(nullptr);
}
};
그것은 무해한 것처럼 보일 수 있습니다. 이 XAML 페이지는 소멸자에서 해당 데이터 컨텍스트를 초기화하려고 합니다. 그러나 DataContext 는 FrameworkElement 기본 클래스의 속성이며 고유한 IFrameworkElement 인터페이스에 있습니다. 따라서 C++/WinRT는 DataContext 속성을 호출하기 전에 QueryInterface에 대한 호출을 삽입하여 올바른 vtable을 조회해야 합니다. 그러나 소멸자에도 있는 이유는 참조 수가 0으로 전환했기 때문입니다. 여기서 QueryInterface을 호출하면 해당 참조 수가 일시적으로 증가합니다. 다시 0으로 돌아가면, 개체는 다시 소멸됩니다.
이를 지원하기 위해 C++/WinRT 2.0이 강화되었습니다. 다음은 간단한 형식으로 릴리스의 C++/WinRT 2.0 구현입니다.
uint32_t Release() noexcept
{
uint32_t const remaining{ subtract_reference() };
if (remaining == 0)
{
m_references = 1; // Debouncing!
T::final_release(...);
}
return remaining;
}
예측한 대로 먼저 참조 수를 줄인 다음 미해결 참조가 없는 경우에만 작동합니다. 그러나 이 항목의 앞부분에서 설명한 정적 final_release 함수를 호출하기 전에 참조 수를 1로 설정하여 안정화합니다. 이를 디바운싱(전기 공학 용어 차용)라고 합니다. 이는 최종 참조가 릴리스되지 않도록 하는 데 중요합니다. 그러한 일이 발생하면 참조 수가 불안정해져서 QueryInterface호출을 안정적으로 지원할 수 없습니다.
최종 참조가 릴리스된 후 QueryInterface 를 호출하는 것은 위험합니다. 참조 수가 무한정 증가할 수 있기 때문입니다. 개체의 수명을 연장하지 않는 알려진 코드 경로만 호출하는 것은 사용자의 책임입니다. C++/WinRT는 QueryInterface 호출을 신뢰성 있게 수행할 수 있도록 하여 여러분과 만나서 함께합니다.
참조 횟수를 안정화하여 이 작업을 수행합니다. 최종 참조가 릴리스되면 실제 참조 수는 0이거나 예측할 수 없는 값입니다. 후자의 경우 약한 참조가 관련된 경우 발생할 수 있습니다. 어느 쪽이든 QueryInterface 대한 후속 호출이 발생하는 경우 지속 불가능합니다. 이는 반드시 참조 수가 일시적으로 증가하므로 디버깅에 대한 참조이기 때문입니다. 1로 설정하면 이 개체에서 릴리스 에 대한 최종 호출이 다시 발생하지 않습니다. 바로 우리가 원하는 것이 이것입니다. 이제 std::unique_ptr가 개체를 소유하게 되었기 때문에, 제한된 QueryInterface와/Release 쌍 호출이 안전하게 이루어질 수 있습니다.
더 흥미로운 예제를 생각해 보세요.
struct MainPage : PageT<MainPage>
{
~MainPage()
{
DataContext(nullptr);
}
static winrt::fire_and_forget final_release(std::unique_ptr<MainPage> ptr)
{
co_await 5s;
co_await winrt::resume_foreground(ptr->Dispatcher());
ptr = nullptr;
}
};
먼저 final_release 함수가 호출되어 구현을 정리할 때임을 알립니다. 여기 final_release는 코루틴입니다. 첫 번째 일시 중단 지점을 시뮬레이션하기 위해 스레드 풀에서 몇 초 동안 대기하여 시작합니다. 그런 다음 페이지의 디스패처 스레드에서 다시 시작합니다.
Dispatcher는 DependencyObject 기본 클래스의 속성이므로 마지막 단계에는 쿼리가 포함됩니다. 마지막으로 nullptr
을(를) std::unique_ptr에 할당함으로써 페이지가 실제로 삭제됩니다. 그런 다음 페이지의 소멸자를 호출합니다.
소멸자 내에서 데이터 컨텍스트를 지웁다. 여기서 알 수 있듯이 FrameworkElement 기본 클래스에 대한 쿼리가 필요합니다.
이 모든 것은 C++/WinRT 2.0에서 제공하는 참조 개수 디바운싱(또는 참조 개수 안정화) 덕분에 가능합니다.
메서드 진입 및 종료 후크
덜 일반적으로 사용되는 확장 지점은 abi_guard 구조체와 abi_enter 및 abi_exit 함수입니다.
구현 형식이 abi_enter함수를 정의하는 경우, 해당 함수는 프로젝션된 각 인터페이스 메서드의 진입 시에 호출됩니다(IInspectable메서드는 제외).
마찬가지로, abi_exit를 정의하면, 그러면 모든 메서드가 종료될 때 그것이 호출됩니다. 그러나 abi_enter가 예외를 발생시키면 호출되지 않습니다. 프로젝션된 인터페이스 메서드 자체에서 예외가 throw되는 경우 여전히 호출됩니다.
예를 들어 클라이언트가 개체를 사용할 수 없는 상태로 전환된 후, 즉 ShutDown 또는 Disconnect 메서드 호출 후에 개체를 사용하려고 시도하면 abi_enter로 가정의 invalid_state_error 예외를 던질 수 있습니다. C++/WinRT 반복기 클래스는 기본 컬렉션이 변경된 경우, abi_enter 함수에서 잘못된 상태 예외를 throw하기 위해 이 기능을 사용합니다.
간단한 abi_enter 및 abi_exit함수 이상에서는 abi_guard 명명된 중첩 형식을 정의할 수 있습니다. 이 경우, 프로젝션된 인터페이스 메서드의 각 항목에 (IInspectable가 아닌 경우) 개체에 대한 참조를 가진 abi_guard 인스턴스가 생성됩니다. 그런 다음 메서드를 종료할 때 abi_guard이 소멸됩니다. 원하는 모든 추가 상태를 abi_guard 유형에 넣을 수 있습니다.
고유한 abi_guard를 정의하지 않으면, 기본적으로 생성 시 abi_enter를 호출하고 소멸 시 abi_exit를 호출하는 기본 보조 기능이 사용됩니다.
이러한 가드는 프로젝션된 인터페이스통해
다음은 코드 예제입니다.
struct Sample : SampleT<Sample, IClosable>
{
void abi_enter();
void abi_exit();
void Close();
};
void example1()
{
auto sampleObj1{ winrt::make<Sample>() };
sampleObj1.Close(); // Calls abi_enter and abi_exit.
}
void example2()
{
auto sampleObj2{ winrt::make_self<Sample>() };
sampleObj2->Close(); // Doesn't call abi_enter nor abi_exit.
}
// A guard is used only for the duration of the method call.
// If the method is a coroutine, then the guard applies only until
// the IAsyncXxx is returned; not until the coroutine completes.
IAsyncAction CloseAsync()
{
// Guard is active here.
DoWork();
// Guard becomes inactive once DoOtherWorkAsync
// returns an IAsyncAction.
co_await DoOtherWorkAsync();
// Guard is not active here.
}