Développer un composant WinRT en C++ : Part II

Dans la 1ère partie de mon post, j’ai évoqué les principes fondamentaux d’un composant WinRT en C++. Dans cette seconde nous allons aborder :

  • La frontière des langages (ABI)
  • Les types commun, le boxing

 

Petit rappel, pour développer un composant WinRT, vous avez deux modèles à votre disposition, le Modèle Windows Runtime Library, ou le modèle C++/CX extension au C++, a été conçu pour supporter plus facilement les nouvelles APIS de Windows 8 le Windows Runtime.

Dans ces différents billets, pour illustrer le développement d'un composant WinRT en C++, j'ai utilisé pour ma part les extensions C++/CX, en conjonction avec une API Win32 pure et dure assez méconnue, Extensible Storage Engine (cf. article Wikipédia https://en.wikipedia.org/wiki/Extensible_Storage_Engine)

J’ai choisi cette API Win32 de base de données, au départ pour un projet interne et pouvoir créer un cache interne, qui bénéficie de toute l’infrastructure d’une base de données ISAM. Avec des indexes primaires, sur plusieurs colonnes, des champs multi-valués etc..

Pour en savoir plus sur les APIs ESE (Jet Blue)  (https://msdn.microsoft.com/en-US/library/windows/apps/br205753 )

Je reviendrai d’ailleurs dans un prochain billet sur l’utilisation de ce composant.

 

La frontière des langages (ABI)

La particularité d’un composant WinRT en C++/CX, c’est qu’il peut être utilisé, à partir des autres langages de développement d’application dite Modern UI de Windows 8. En C++/CX, bien évidement, mais également en JavaScript, en C# ou VB.NET.

Mais ne pourra être exposé à l’extérieure du composant, que les types qui sont compréhensibles par les autres langages. De toute manière le compilateur vous indiquera ce qui n’est pas possible d’exposer, avec un message du type:

error C3984: 'JetBlue::WinRT::JetBlueTable' : a non-value type cannot have any public data members 'Count' (JetBlueDatabase.cpp)    

Néanmoins, lorsqu’on reste dans les frontières du C++ , il est possible de mixer, du C++/CX avec de l’ISO C++ , et des librairies traditionnelles C++ telle que la STL (Standard Type Library) comme sur l’extrait de code suivant :

JETWINRT::JetBlueTable^ JETWINRT::JetBlueDatabase::OpenTable(Platform::String^ tablename,OpenTableFlagsflags)
{
        std::wstring tableName(tablename->Data());
        std::shared_ptr<JETWIN32::JetBlueTable> table;
        m_JetError->RaiseOnError(m_Win32Database->OpenTable(tableName,static_cast<JET_GRBIT>(flags),table));        
        //Open the system table to get the indexName from the column name        
        Map<String^,String^>^ IndexName= InternalOpenSystemTable(tableName);
        
        returnrefnewJetBlueTable(table,tablename,IndexName,m_JetError);
}

On distingue ici, l’utilisation de types WinRT (Platform::String^ et JETWINRT::JetBlueTable^) qui sont des références symbolisées par le caractère ^, avec des types et des bibliothèque propres au C++, comme std::wstring,

Pour instancier ou activer une classe WinRT  on utilisera le mot clé ref new, pour bien le distinguer du mot clé new.

Remarque : Pour déférencer un composant WinRT on utilisera le symbole % par opposition au caractère & de l’ISO C++.

Dans l’architecture de votre composant, il est donc tout à fait possible de mixer par exemple une librairie existante ISO C++, avec un Composant WinRT, qui pour ce dernier, ne sera qu’une enveloppe autour de votre librairie.

Autant que faire se peut, il faut réduire les appels entre les frontières des langages, et ceci pour des raisons de performances. Il est donc important que la logique soit faite le plus en C++.

Dans mon composant, j’ai pris le partie de créer une librairie statique C++, (JetBlue.Win32.lib), qui expose un modèle objet ISO C++ simple et qui fait appel directement à la librairie esent.lib, et l’enveloppe JetBlue.WinRT qui expose un modèle objet aux autres langages.

Dans l’exemple de code ci-dessus on voit bien que la méthode JETWINRT::JetBlueDatabase::OpenTable(), appel son pendant Win32, m_Win32Database->OpenTable(), et retourne à l’appelant une classe JetBlueTable^.

Alors que la méthode OpenTable de ma librairie Win32, ne manipule que des types qui ne soit pas WinRT et fait appel directement aux fonctions de ESE. De ce faite je peux la réutiliser avec des applications Desktop Traditionnelles.

HRESULTJetBlueTable::OpenTable(std::wstring& tablename,JET_GRBITflags,std::shared_ptr<JetBlueTable> &table)
{
    HRESULT hr=S_OK;
    JET_TABLEID TableID=JET_tableidNil;
    JET_PCWSTR TableName=tablename.c_str();        
    hr=m_jetBlueErr->CheckError(::JetOpenTableW(m_SessionID,m_DBID,TableName,NULL,0,flags,&TableID));
    if (FAILED(hr))
        return hr;

    table=std::make_shared<JetBlueTable>(m_SessionID,m_DBID,TableID,tablename,m_jetBlueErr);

    return hr;
}

JetOpenTableW est une méthode C de la librairie esent.lib

Vous noterez également que j’ai pris le partie de retourner pour la gestion d’erreur un HRESULT, nous en reparlerons dans un prochain billet.

Dans une vue plus générale de notre exemple ci-dessus, j’ai une entrée (Platform::String^ ) que je converti en std::wstring que je passe à ma librairie native, c’est le principe général que j’utilise pour toutes l’architecture de mon composant.

En terme de performance, il faudra jeter un œil sur tous vos algorithmes pour éviter la copie de données. Je ne peux d’ailleurs que vous encourager à suivre la présentation de Sridhar Madhugiri sur le sujet : C++/CX Best Practices et qui est truffée de bonnes astuces. Mais si vous devez faire de la manipulation de chaines, il est est préférable d’utiliser les types natifs tel que std::wstring, plutôt que Platform::String^.
En règle générale, il est préférable autant que faire ce peux dans vos algorithmes d’utiliser les  types natifs plutôt que les types WinRT.

Si vous souhaitez exposer des collections aux autres langages, il vous devez utiliser les interfaces adéquates.

Par exemple .NET map automatiquement certaines interfaces en interfaces WinRT

image

Code C#

staticasyncTask<JetBlueTable> CreateTableExpansionAsyncSimple(String tablename, JetBlueDatabase db)
        {
            List<JetBlueColumn> Columns = newList<JetBlueColumn>();
            Columns.Add(newJetBlueColumn("ExpansionName", ColumnType.Text, ColumnFlags.NotNull, "idxExpansionName", IndexFlags.Unique));
            Columns.Add(newJetBlueColumn("ExpansionId", ColumnType.Int32, ColumnFlags.NotNull, "idxExpansionId", IndexFlags.Unique));
            Columns.Add(newJetBlueColumn("BlockId", ColumnType.Int32, ColumnFlags.NotNull, "idxBlockId", IndexFlags.Null));
            Columns.Add(newJetBlueColumn("ExpansionPicturePath", ColumnType.Text, ColumnFlags.MaybeNull));            
            Columns.Add(newJetBlueColumn("Picture", ColumnType.LongBinary, ColumnFlags.MaybeNull));           
            var table = await db.CreateTableAsync(tablename, Columns);
            return table;
        }

La collection List<T> de .NET dérivant de IList<T>, .NET la transformera automatiquement en son pendant WinRT  IVector<T>

 

Code Javascript

var db = engine.openDatabase("URZA.EDB", JetBlue.WinRT.Enum.OpenDatabaseFlags);
        Columns = [];
        Columns.push(new JetBlueColumn("ExpansionName", ColumnType.Text, ColumnFlags.NotNull, "idxExpansionName", IndexFlags.Unique));
        Columns.push(new JetBlueColumn("ExpansionId", ColumnType.Int32, ColumnFlags.NotNull, "idxExpansionId", IndexFlags.Unique));
        Columns.push(new JetBlueColumn("BlockId", ColumnType.Int32, ColumnFlags.NotNull, "idxBlockId", IndexFlags.Null));
        Columns.push(new JetBlueColumn("ExpansionPicturePath", ColumnType.Text, ColumnFlags.MaybeNull));            
        Columns.push(new JetBlueColumn("Picture", ColumnType.LongBinary, ColumnFlags.MaybeNull));           
        var table = db.CreateTable(tablename, Columns);
        

Javascript quand à lui projette un tableau en IVector<T>.

Code C++/CX

IAsyncOperation<JETWINRT::JetBlueTable^>^ JETWINRT::JetBlueDatabase::CreateTableAsync(String^ tablename,
                                                                                      IVector<JetBlueColumn^>^ columns)
{
    return create_async([this,tablename,columns]()
    {
        return CreateTable(tablename,columns);
    });
}

 

Veuillez également noter que Javascript ne gère pas la surcharge de méthodes à la différence des autres langages, il est donc important de lui notifier quelle méthode il verra par défaut, avec l’attribut [DefaultOverload]

    ///<summary>Create a table in the Database asynchronously</summary>
                ///<param name='tablename'>The name of the table</param>
                ///<param name='columns'>A list of JetBlueColumn object</param>
                [DefaultOverload]
                IAsyncOperation<JETWINRT::JetBlueTable^>^ CreateTableAsync(Platform::String^ tablename,
                                                                           IVector<JetBlueColumn^>^ columns);
                IAsyncOperation<JETWINRT::JetBlueTable^>^ CreateTableAsync(Platform::String^ tablename,
                                                                           IVector<JetBlueColumn^>^ columns,
                                                                           IVector<JetBlueIndex^>^ indexes);

Seule la 1ere méthode sera visible et appelable à partir de Javascript, si vous considérez que la seconde est aussi importante, alors il faudra la renommer.

Dans mon exemple, j’ai pris le partie d’utiliser dans la signature des méthodes et des propriétés le type Object^ , et ceci pour la raison essentielle, que les APIS esent, utilisent majoritairement des void* . Il faut alors “Caster” ou Boxer dans le jargon WinRT le type Object^ dans le type correspondant, c’est ce que nous allons voir à la section suivante.

Les types commun, le boxing

Chacun s’accordera à dire que le Boxing  est consommateur et à proscrire quoi qu’il arrive et je suis d’accord. Néanmoins, partir sur cette stratégie m’a évité de créer une méthode, ou une propriété pour chaque type de colonnes d’une table.

Par exemple ma classe JetBlueColumn^ au lieu de définir X propriétés en fonction du type que je veux dans ma table, j’ai défini une bonne fois pour toute une propriété Value de type Object^. Il faut que le composant soit le plus simple à utiliser dans les autres langages. Néanmoins si mon niveau d’attente des performances n’est pas atteint alors je changerai de stratégie.

publicrefclassJetBlueColumnsealed
        {
             ///<summary>Field to use when inserting or retrieving data from the table</summary>
            ///<remarks>The type of this field is Object, this means that you can
            ///insert or retrieve any type of data (Int32 to DateTime)</remarks>
            property Platform::Object^ Value;

et en interne je fais le cast en fonction du type de la colonne avec static_cast<> .

caseJET_coltypLong:    
            {
                Platform::Type^ type=value->GetType();
                auto typeCode=type->GetTypeCode(type);
                    if (typeCode !=TypeCode::Int32)
                    {
                        m_JetError->RaiseOnError(m_Win32Table->ThrowInvalidColumnType());
                    }
                INT32 v=static_cast<INT32>(value);

Ici la variable value, est de type Object^ , qui contient une valeur entière de type TypeCode::Int32, que je peux donc convertir ou boxer en une valeur entière native INT32.

Ce boxing est  d’ailleurs automatiquement fait par le Runtime comme illustré dans le code suivant :

template <typename__TArg>
                __TArg __abi_unbox(::Platform::Object^ __objArg)
                {
                    returnsafe_cast< ::Platform::Box<__TArg>^>(__objArg);
                }

Si jamais value contenait un TypeCode::Int64, la ligne static_cast<INT32> remonterait l’erreur “No such interface supported”.

 

Pour la conversion inverse c’est plus simple, car il suffit d’affecter la variable de type INT32 à la variable de type Object^.

    INT32 entier=1234;
    Object^ value=entier;

et le unboxing ce fait automatiquement également

template <typename__TArg>
Platform::Box<typename ::Platform::Details::RemoveCV<__TArg>::Type>^ __abi_create_box(__TArg__boxValueArg)
{
    returnrefnew ::Platform::Box<__TArg>(__boxValueArg);
}

 

Pour une chaine de caractères String^ , c’est aussi simple, car elle contient une méthode String::Data() qui retourne une const wchar_t*, mais attention ici c’est une copie qui est fait.

    String^ s="Je suis une chaine";        
    constwchar_t *d=s->Data();

Pour le unboxing et pour éviter la copie inutile d’une chaine native de type wchar_t, on utilisera StringReference<T> de préférence.

constwchar_t *wc=L"Je suis une chaine";    
    //Pas de copie de la chaine
    StringReference sr(wc);    
    Object^ o1=sr;

 

Le second exemple fait une copie inutile de la chaine.

constwchar_t *wc=L"Je suis une chaine";    
    //Copie de la chaine;
    String^s=refnewString(wc);
    Object^ o2=s;

 

Le même principe s’applique au tableau (Array), pour éviter les copies inutiles, vous pouvez utiliser la classe ArrayReference<T>

Je ne vais pas parcourir tous les types de la WinRT, mais il y en a un qui est important dans une base de données, c’est le type TypeCode::DateTime, et qui diffère de celui de .NET.

En effet en .NET nous aurions tendance à utiliser la classe System.DateTime, mais elle ne correspond pas au TypeCode::DateTime de la WinRT. A la place il faut utiliser la classe System.DateTimeOffset comme illustré dans le code C# suivant :

//Ici date n'est pas de type TypeCode::DateTime
                Object date = newDateTime(1964, 12, 16);
                
                //Ici date est de type TypeCode::DateTime
                Object date = newDateTimeOffset(newDateTime(1964, 12, 16));

Dans la base de données je vais sauvegarder un INT64 représentant le temps universel.

Pour ce faire, je cast l’objet vers la classe Windows::Foundation::DateTime, puis cast le membre UniversalTime en INT64

 

Platform::Type^ type=value->GetType();
                auto typeCode=type->GetTypeCode(type);
                if (typeCode !=TypeCode::DateTime)
                {
                    m_JetError->RaiseOnError(m_Win32Table->ThrowInvalidColumnType());
                }
                auto dateTime=static_cast<Windows::Foundation::DateTime>(value);                    
                INT64 dateToSave=static_cast<INT64>(dateTime.UniversalTime);

Pour l’opération inverse c’est aussi simple que le code suivant:

Windows::Foundation::DateTime dateTime;                            
                        dt.UniversalTime=safe_cast<INT64>(value);
                        Object^ o=dt;

 

Dans la 3ième partie nous évoquerons

  • Les exceptions
  • Asynchronisme
  • Levée d'évènement et synchronisation de thread

 

Eric