Consumare un C++/WinRT Runtime Component in un'applicazione Win32

Part 1



Introduzione a C++/WinRT.

 Basata sul C++ Standard 17, C++/WinRT è attualmente la più moderna tecnologia per la creazione di applicazioni (C++) in ambiente Windows. Il fatto che sia compatibile col C++ Standard 17 significa che può essere usata anche per le classiche applicazioni C++ le quali avranno accesso così, alle più moderne API del sistema operativo.
Si possono creare, quindi, soluzioni "miste" composte da progetti UWP-C++/WinRT -Win32 -.Net che interagiscono tra loro consentendoci di sfruttare il meglio delle varie tecnologie.
Fino a qualche tempo fa tutto ciò non era possibile se non con COM od attraverso la creazione di proxy in C++/CX (C++ Extended), un dialetto C++ creato da Microsoft con una sintassi simile al linguaggio C#.
Il C++/CX è stato messo da parte una volta per tutte e da ora in poi con C++/WinRT si torna ad usare il C++ Standard.


Cos'è esattamente C++/WinRT.
language implementation & language projection

Prima di tutto è necessario chiarire cosa è WinRT language projection.
Possiamo definire il language projection la capacità del sistema operativo di esporre le Windows Runtime APis in maniera omogenea (anzi identica) ed automatica, indipendentemente da quale linguaggio di programmazione sia stato usato per implementare le applicazioni o le APIs stesse.
Dal punto di vista tecnico, il sistema operativo legge dei metadati ed attraverso COM attiva il corretto language projection.
L'idea alla base è simile a quella relativa a Component Object Model (COM), infatti anche in COM un componente una volta pubblicato, indipendentemente dal linguaggio in cui è stato scritto, può essere utilizzato in maniera trasparente da applicazioni scritte in un qualunque linguaggio COM compatibile.

Quindi in WinRT così come in COM, le applicazioni possono essere scritte in vari linguaggi (language implementation: C++, C#, VB, Delphi etc.); ogni linguaggio, o per meglio dire ogni compilatore, in questo caso non produce solo DLL.s o EXE ma anche files .winmd (WinMD) ovvero Windows Metadata, i quali contengono la descrizione, in codice macchina, dei tipi implementati (i metadati).
I metadati, quando richiesto, vengono letti e utilizzati da tools di sviluppo software e/o dal sistema operativo a runtime attivando il language projection specifico per un dato linguaggio; per cui, nel caso del C++, quando il sistema operativo troverà codice C++/WinRT (compilato in un file .winmd) attiverà il language projection per il linguaggio C++.
La stessa cosa avviene, ovviamente, per ogni rispettivo linguaggio sia stato usato al momento dello sviluppo delle applicazioni.

Tecnicamente C++/WinRT è una libreria template C++ (standard 17) per Windows Runtime Platform basata sul pattern CRTP (curiously recurring template pattern) e distribuita interamente in file headers.


Il fine di un Windows Runtime Component, normalmente, è quello di essere "consumato" da varie applicazioni; caratteristica, questa, che accomuna WinRT components agli oggetti COM.
(Con normalmente intendo dire che questo è lo scopo naturale, tuttavia niente impedisce di creare un WinRT component ed consumarlo esclusivamente all'interno di un'unica applicazione; ovviamente in tal caso non sarebbe necessario creare un componente WinRT).

Il fatto che un WinRT Component possa essere consumato da applicazioni scritte in vari linguaggi comporta, così come avviene in COM, la necessità di descrivere in un linguaggio universale standardizzato i tipi, le proprietà, classi, metodi etc..
Il "luogo" in cui tale descrizione, ancora una volta, come per COM, andrà scritta, è uno o più files IDL.
Ne consegue che, anche in C++/WinRT, una runtime class che debba essere consumata, oltre che dalla nostra, anche da altre apps, deve essere obbligatoriamente dichiarata in un file .idl.
Questo è necessario affinchè i tipi dichiarati nei files .idl vengano poi esportati dallo strumento MIDL nel file compilato .winmd.


Struttura iniziale del file IDL in C++/WinRT.

Creando un nuovo progetto WinRT Component in Visual Studio 2019 troviamo anche un file IDL: Class.idl; il cui contenuto è il seguente:

        namespace UWPUtils
        {
        [default_interface]
        runtimeclass Class
        {
        Class();
        Int32 MyProperty;
        }
        }

    

Nella dichiarazione della classe possiamo notare che è decorata con l'attributo [default_interface] e definita con la keyword runtimeclass (il cui significato è ovvio).
Sempre nel file .idl, vediamo che è presente un costruttore predefinito; per ogni costruttore dichiarato nel file .idl il compilatore genera i tipi di implementazione e i tipi proiettati (implementation type e projected type), in pratica scrive uno stub per i nostri sources già bello e pronto all'uso.
I costruttori definiti nel file .idl verranno utilizzati dalle applicazioni al momento di consumare la runtime class al di fuori della propria unità di compilazione.



Properties, or not properties, that is the question.
A proposito di proprietà

Nel file .idl iniziale proposto da Visual Studio viene definita, oltre al costruttore predefinito una proprietà (MyProperty); però non viene specificato se questa proprietà è read only o meno.
Quando non specificato, lo strumento MIDL produrrà i rispettivi metodi get e set (ovvero get e set sono impliciti).
Chi conosce il C++ standard potrebbe rimanere perplesso, perchè più che ad un'applicazione C++, questo file .idl sembra riferirsi a qualche altro linguaggio (Java o C# in primis); infatti nel linguaggio C++ standard non è possibile definire in una classe delle proprietà come le intendiamo in C#, Java, Delphi e via dicendo, corredate dei relativi getter e setter; pertanto in C++/WinRT per ogni proprietà descritta nel file IDL verrà prodotto dallo strumento MIDL un metodo di get e set (vedi esempi qui sotto).

Dichiarazione di una classe C#

 
    // Class with properties in C#
    class DemoStudent
    {
        
        // default ctor
        public DemoStudent() { }
        
        //properties
        public string FirstName { get; set; }
        public string LastName  { get; set; }

        //other code
        ................
        ................

    }
        
        

La dichiarazione della stessa classe in C++ standard 17


     // Class without properties in standard C++ 17
     class DemoStudent
     {

         public:
       
         //default ctor
         DemoStudent() = default;

         //default dtor
         virtual ~DemoStudent() = default;
      
         // getter/setter or accessor/mutator if you prefer
         void set_FirstName(const std:wstring& name);
         const std::wstring get_FirstName() const;

         void set_LastName(const std:wstring& last_name);
         const std::wstring get_LastName() const;

         //other code
        ................
        ................


        private:
       
        std::wstring m_name;
        std::wstring m_last_name;

         //other code
        ................
        ................

     };

        

Deviando invece un pò dal C++ standard e usando Microsoft Visual C++ possiamo definire delle proprietà con una estensione del linguaggio C++:

  
    // Class with properties in standard C++ 17 thanks to Visual C++ extension  
    class DemoStudent()    
    {

    public:  

    //Default ctor/dtor
    DemoStudent() = default;    
    virtual ~DemoStudent() = default;


    //getters/setters    
     std::wstring get_FirstName(){ return m_FirstName;}
     void put_FirstName(std::wstring fname){ m_FirstName = fname;}

     std::wstring get_LastName(){ return m_LastName;}
     void put_LastName( std::wstring lname){ m_LastName = lname;}

    
    // Properties declaration:
    // FirstName
    // LastName
    __declspec(property(get = get_FirstName, put = put_FirstName)) std::wstring FirstName;
    __declspec(property(get = get_LastName, put = put_LastName)) std::wstring LastName;


         //other code
        ................
        ................

    private:
    std::wstring m_FirstName;
    std::wstring m_LastName;

    };

    



Properties in C++/WinRT
In C++/WinRT le proprietà anzichè nelle classi vengono definite nel rispettivo file .idl e in compilazione MIDL produce setters e getters.

C++/WinRT class with properties

// file DemoStudent.idl
// Interface declaration in MIDL 3.0
namespace UWPUtils
{


    [default_interface]
    runtimeclass DemoStudent
    {
        
        //default ctor
        DemoStudent();
        
        //Properties
        String FirstName; //implicit get;set;
        String LastName;  //implicit get;set;
       
        //other code
        ................
        ................
        
    }


}

  
// file DemoStudent.h 
// Class implementation with properties in C++/WinRT      
namespace winrt::UWPUtils::implementation
{
    struct DemoStudent : DemoStudentT<DemoStudent>
    {

    //default ctor
    DemoStudent() = default;

    //get
    hstring FirstName();
    //set
    void FirstName(hstring const& value);

    //get
    hstring LastName();
    //set
    void LastName(hstring const& value);

    //other code
    ................
    ................

    };
   
}

namespace winrt::UWPUtils::factory_implementation
{
     struct DemoStudent : DemoStudentT<DemoStudent, implementation::DemoStudent>
     {

     };
}





Struttura di una runtime class

Nel file .h qui sopra possiamo notare che la runtime class è in realtà una struct; la differenza com'è noto è che in una struct i membri ed i metodi sono public di default.
Possiamo vedere anche che la nostra runtime class (struct DemoStudent qui sopra) eredita da una classe base template (DemoStudentT) che come argomento ha la classe runtime stessa.
Come sappiamo, in C++ quando una classe eredita una classe template nel cui argomento/i vi è la classe derivata stessa, ci troviamo di fronte al caso di Curiously Recurring Template Pattern (CRTP) o F-bound polymorphism pattern (vedi un esempo qui sotto) e C++/WinRT è basato proprio su questo idioma.


// Curiously Recurring Template Pattern (CRTP) o F-bound polymorphism pattern sample
// A derived class inherits from a template class with itself as template argument

// Template base class
template <class T>
class BaseT
{
public:
	BaseT() = default;
	virtual ~BaseT() = default;
	
	std::wstring Run() { 
		 static_cast<T*>(this)->Execute();
		 return L"Method Execute called from base!";
	    }

};


// CRTP derived class
class Derived : public BaseT<Derived>
{
public:
    Derived() = default;
    virtual ~Derived() = default;

    void Execute() { result = L"Method Execute called from derived!"; Print();  }

private:
    std::wstring result = L"";
    void Print() { std::wcout << result << std::endl; }

};


    //------------------------------------------------

// main
int main()
{

auto d = std::make_unique<Derived>();

std::wstring str = d->Run();

std::wcout << str << std::endl;

d = nullptr;
d.reset();


return 0;

};


    //------------------------------------------------

    //output:
    Method Execute called from derived!
    Method Execute called from base!





Implementation types
Come già detto, nel nostro caso è necessario dichiarare nel file .idl la nostra runtime class, fatto ciò al momento del build il toolchain (midl.exe e cppwinrt.exe) genera per noi un implementation type.
Si tratta dello stub della nostra struct a cui ho fatto riferimento più su.
Lo struct stub generato da Visual Studio, basato sulla runtime class che abbiamo dichiarato nel file .idl è pronto per l'uso ed è salvato una parte in un file .h e l'altro in file .cpp; possiamo volendo fare copia/incolla dei due files dalla cartella sources a quella del nostro progetto.
I due files generati sono nella cartella ..project\Generated Files\sources\ il contenuto dei quali è:

Implemented types sources



// file DemoStudent.h
#pragma once
#include "DemoStudent.g.h"

// Note: Remove this static_assert after copying these generated source files to your project.
// This assertion exists to avoid compiling these generated source files directly.
//static_assert(false, "Do not compile generated C++/WinRT source files directly");

namespace winrt::UWPUtils::implementation
{
    struct DemoStudent : DemoStudentT<DemoStudent>
    {
        DemoStudent() = default;

        hstring FirstName();
        void FirstName(hstring const& value);
        hstring LastName();
        void LastName(hstring const& value);
    };
}
namespace winrt::UWPUtils::factory_implementation
{
    struct DemoStudent : DemoStudentT<DemoStudent, implementation::DemoStudent>
    {
    };
}




// file DemoStudent.cpp
#include "pch.h"
#include "DemoStudent.h"
#include "DemoStudent.g.cpp"

// Note: Remove this static_assert after copying these generated source files to your project.
// This assertion exists to avoid compiling these generated source files directly.
//static_assert(false, "Do not compile generated C++/WinRT source files directly");

namespace winrt::UWPUtils::implementation
{
    hstring DemoStudent::FirstName()
    {
        throw hresult_not_implemented();
    }
    void DemoStudent::FirstName(hstring const& value)
    {
        throw hresult_not_implemented();
    }
    hstring DemoStudent::LastName()
    {
        throw hresult_not_implemented();
    }
    void DemoStudent::LastName(hstring const& value)
    {
        throw hresult_not_implemented();
    }
}


Il resto dell'implementazione dei tipi generata da Visual Studio (ammettendo che il file si chiami DemoStudent) come nell'esempio è nei file
Implemented types sources



//file DemoStudent.g.h

// WARNING: Please don't edit this file. It was generated by C++/WinRT v2.0.201113.7

#pragma once
#include "winrt/UWPUtils.h"
namespace winrt::UWPUtils::implementation
{
    template <typename D, typename... I>
    struct __declspec(empty_bases) DemoStudent_base : implements<D, UWPUtils::DemoStudent, I...>
    {
        using base_type = DemoStudent_base;
        using class_type = UWPUtils::DemoStudent;
        using implements_type = typename DemoStudent_base::implements_type;
        using implements_type::implements_type;
        
        hstring GetRuntimeClassName() const
        {
            return L"UWPUtils.DemoStudent";
        }
    };
}
namespace winrt::UWPUtils::factory_implementation
{
    template <typename D, typename T, typename... I>
    struct __declspec(empty_bases) DemoStudentT : implements<D, Windows::Foundation::IActivationFactory, I...>
    {
        using instance_type = UWPUtils::DemoStudent;

        hstring GetRuntimeClassName() const
        {
            return L"UWPUtils.DemoStudent";
        }
        auto ActivateInstance() const
        {
            return make<T>();
        }
    };
}

#if defined(WINRT_FORCE_INCLUDE_DEMOSTUDENT_XAML_G_H) || __has_include("DemoStudent.xaml.g.h")
#include "DemoStudent.xaml.g.h"
#else

namespace winrt::UWPUtils::implementation
{
    template <typename D, typename... I>
    using DemoStudentT = DemoStudent_base<D, I...>
}

#endif




// file DemoStudent.g.cpp

// WARNING: Please don't edit this file. It was generated by C++/WinRT v2.0.201113.7

void* winrt_make_UWPUtils_DemoStudent()
{
    return winrt::detach_abi(winrt::make<winrt::UWPUtils::factory_implementation::DemoStudent>());
}
WINRT_EXPORT namespace winrt::UWPUtils
{
    DemoStudent::DemoStudent() :
        DemoStudent(make<UWPUtils::implementation::DemoStudent>())
    {
    }
}




// file module.g.cpp

// WARNING: Please don't edit this file. It was generated by C++/WinRT v2.0.201113.7

#include "pch.h"
#include "winrt/base.h"
void* winrt_make_UWPUtils_DemoStudent();

bool __stdcall winrt_can_unload_now() noexcept
{
    if (winrt::get_module_lock())
    {
        return false;
    }

    winrt::clear_factory_cache();
    return true;
}

void* __stdcall winrt_get_activation_factory([[maybe_unused]] std::wstring_view const& name)
{
    auto requal = [](std::wstring_view const& left, std::wstring_view const& right) noexcept
    {
        return std::equal(left.rbegin(), left.rend(), right.rbegin(), right.rend());
    };

    if (requal(name, L"UWPUtils.DemoStudent"))
    {
        return winrt_make_UWPUtils_DemoStudent();
    }

    return nullptr;
}

int32_t __stdcall WINRT_CanUnloadNow() noexcept
{
#ifdef _WRL_MODULE_H_
    if (!::Microsoft::WRL::Module<::Microsoft::WRL::InProc>::GetModule().Terminate())
    {
        return 1;
    }
#endif

    return winrt_can_unload_now() ? 0 : 1;
}

int32_t __stdcall WINRT_GetActivationFactory(void* classId, void** factory) noexcept try
{
    std::wstring_view const name{ *reinterpret_cast<winrt::hstring*>(&classId) };
    *factory = winrt_get_activation_factory(name);

    if (*factory)
    {
        return 0;
    }

#ifdef _WRL_MODULE_H_
    return ::Microsoft::WRL::Module<::Microsoft::WRL::InProc>::GetModule().GetActivationFactory(static_cast<HSTRING>(classId), reinterpret_cast<::IActivationFactory**>(factory));
#else
    return winrt::hresult_class_not_available(name).to_abi();
#endif
}
catch (...) { return winrt::to_hresult(); }



Beh insomma bisogna ammettere che a prima vista il codice creato da Visual Studio potrebbe intimidire, in realtà non è niente di complicato.
Partendo dal file DemoStudent.g.h, all'inizio dopo il namespace troviamo la dichiarazione di una classe variadic template, ovvero una struct (in questo caso, ma anche una funzione può essere variadic template) che supporta un numero arbitrario di argomenti (proprio come la funzione printf del C) il cui nome è DemoStudent_base.
Questa è la nostra classe (struct) base.
La struct DemoStudent_base eredita la struct winrt::implements, anch'essa una variadic template class (struct), che ha tra gli altri argomenti, UWPUtils::DemoStudent.
La struct winrt::implements è la base struct da cui ogni nostra runtime class o activation factory, direttamente o indirettamente discendono; questa implementa varie interfacce fondamentali tra cui IUnknown, IInspectable etc..
Il codice sucessivo è una serie di using new_type_name= ovvero alias di tipi già esistenti (C++ 17) e uso di namespaces.
Di seguito troviamo un metodo GetRuntimeClassName()che restituisce un valore hstring.
Con hstring (winrt::hstring) si intende una struct che contiene una serie di caratteri UTF-16 Unicode; ovvero stringhe di testo.

Infine troviamo il metodo ActivateInstance che a seconda della classe passatagli con l'argomento T chiamando la funzione template make<T> ritorna:

a) se stiamo creando un winrt component che è consumato da app esterne all'unità di compilazione in cui componente è implementato: ritorna (project) l'interfaccia di default del tipo implementato.

b) se stiamo implementando e consumando una runtime class all'interno della stessa unità di compilazione: ritorna un'istanza del projected type.

Stesso discorso vale per la struct DemoStudentT, ma questa volta nel namespace factory_implementation.

Infine, nel namespace implementation viene dichiarato un alias per la struct DemoStudent_base con la direttiva (C++ 17)
using DemoStudentT = DemoStudent_base<D, I...>







Giuseppe Pischedda 2020


Se il post ti è utile puoi fare una donazione all'autore, l'importo è a tua libera scelta.

Grazie