Développer un composant WinRT en C++ : Part III
Dans nos deux précédents billets, Part I et Part II, nous avons évoqué rapidement ce qu’était un composant WinRT, les différentes techniques de développement, comment est activé un composant et comment sont exposés les types WinRT dans les autres langages.
Dans cette dernière partie, nous allons aborder :
- Les exceptions
- L’asynchronisme
- La levée d’évènement et la synchronisation de thread.
Les Exceptions
La gestion des erreurs dans un composant WinRT est basée essentiellement sur les exceptions contenues dans l’espace de nom Platform.
Vous pouvez au choix utiliser, Platform ::Exception, Platform::ComException, Platform::FailureException, ou la méthode Platform::Exception::CreateException(), comme illustré dans l’extrait de code suivant :
Code Snippet
- HRESULT hr =E_FAIL;
- throw Platform::Exception::CreateException(hr, "Erreur CreateException");
- throw ref new Platform::COMException(hr, "Erreur COM");
- throw ref new Platform::FailureException("Erreur Failure Exception");
- throw ref new Platform::Exception(hr, "Erreur Juste une exception");
Comme on peut le voir, les exceptions prennent deux paramètres un HRESULT et une chaine de caractères.
Le HRESULT dans le cas de notre composant basé sur ESE, doit pouvoir convertir les numéros d’erreur JET_ERR en HRESULT, pour ce faire nous allons utiliser une bonne vieille macro MAKE_HRESULT, comme illustré dans le code suivant :
Code Snippet
- HRESULT JetBlueErr::MakeJetErrToHRESULT(JET_ERR err)
- {
- unsigned long const FACILITY_ESE = 0xE5E;
- unsigned long const STATUS_SEVERITY_ERROR=0x3;
- if (err==JET_errOutOfMemory)
- {
- return E_OUTOFMEMORY;
- }
- else if (err<0)
- {
- return MAKE_HRESULT(STATUS_SEVERITY_ERROR, FACILITY_ESE, -err & 0xFFF);
- }
- else
- {
- return MAKE_HRESULT(STATUS_SEVERITY_ERROR, FACILITY_ESE, err & 0xFFF);
- }
- }
Vous trouverez toutes les informations nécessaire sur la gestion des erreurs COM à cette adresse je ne reviens donc pas dessus :
https://msdn.microsoft.com/en-us/library/windows/desktop/ms679692(v=vs.85).aspx
Le second paramètre, la chaine de caractères en réalité ne sert pas à grand-chose avec Windows 8, si ce n’est, pour vous aider à debugger. En effet seul le debugger est capable de l’afficher, mais en aucun cas l’application appelante. En d’autres termes, cette chaine, ne traverse pas les frontières des langages, et on ne récupère qu’un message laconique “The text associated with this error code could not be found”
Remarques :
Il existe un certain nombre d’exception standard qui traversent la frontière des langages qui sont décrites ici. D’autre part, il n’est pas possible non plus de dériver sa propre exception.
Alors comment pouvoir récupérer cette chaine de caractères avec Windows 8 ?
Il est important de remonter un HRESULT qui soit cohérent, et qui permettra à l’appelant de savoir en fonction de ce HRESULT quelle est l’erreur exacte. Néanmoins, ce n’est pas forcement l’optimum, car l’appelant doit donc avoir sa propre table de correspondance HRESULT<->Message, que vous lui aurez fourni à l’aide de la documentation.
Néanmoins, ce n’est pas forcement l’optimum, un autre mécanisme est de stocker le message d’erreur et de fournir une méthode à l’ancienne du type GetLastErrorMessage(), c’est ce que j’ai employé dans mon composant, jusqu’à l’arrivée de Windows 8.1.
Le dernier mécanisme est la gestion d’un évènement OnJetBlueError, comme nous le verrons par la suite.
Petit digression :
Il nous faut convertir notre composant Windows 8 en Windows 8.1, pour ce faire il faut Visual Studio 2013 qui tourne sur un Windows 8.1 bien évidement.
- Faite une copie de son code Windows 8, on ne sait jamais.
- Chargez le projet Windows 8 avec Visual Studio 2013
- Cliquez sur le bouton droit de la souris au niveau du projet et choisir “Retarget to Windows 8.1” .
Remarque :
Pour ma part, il a fallu que je corrige un bien curieux message d’erreur C2872 : ‘Platform’ : ambiguous symbol.
En effet ils ont eu la malice de rajouter dans l’espace de nom Windows::Foundation::Metadata, une énumération nommée Platform. Pour résoudre le problème, ne pas déclarer cet espace de nom dans le using namespace, mais préfixez tous les attributs directement. Comme illustré dans le code suivant :
Code Snippet
- [Windows::Foundation::Metadata::DefaultOverload]
- void SetKey(IVector<JetBlueKey^>^ keys);
Une fois le projet converti, la méthode GetLastErrorMessage, devient obsolète, puisqu’on récupère directement la chaine dans la propriété Message de l’exception.
Remarque :
En réalité, deux messages d’erreur sont accolés, qu’il faut dissocier pour retrouver le message d’erreur du composant, comme illustrer dans le code suivant :
Code Snippet
- catch (Exception ex)
- {
- var indexOf = ex.Message.LastIndexOf("\n")+1;
- var Message = ex.Message.Substring(indexOf, ex.Message.Length - indexOf);
- txtDebug.Text = Message;
- }
Néanmoins à l’heure ou je vous écrit, je vais rester avec ma bonne vieille méthode GetLastErrorMessage(), car il semblerait que la WinRT ne remonte pas tout le temps le message correctement ou alors cela n’est pas prévu lors de l’exécution d’une méthode Asynchrone.
Je reviendrai sur ce point très bientôt
L’asynchronisme.
Je ne vais pas rentrer dans le détail de l’asynchronisme, car j’ai déjà tout expliqué dans le billet Asynchronisme et Scenarios Hybrides.
En faite l’idée lors du développement d’un composant WinRT est de développer ou de réutiliser, une méthode synchrone, que l’on va encapsuler le plus simplement du monde, dans un appel asynchrone. C’est la méthode que j’ai majoritairement utilisée lors du développement de mon composant.
Tout d’abord, j’implémente, je test et je peaufine une méthode synchrone pour ensuite l’encapsuler dans une méthode asynchrone.
Prenons par exemple la méthode OpenTable qui retourne une instance de la classe JetBlueTable^
Code Snippet
- JETWINRT::JetBlueTable^ JETWINRT::JetBlueDatabase::OpenTable(Platform::String^ tablename,OpenTableFlags flags)
- {
- 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);
- return ref new JetBlueTable(table,tablename,IndexName,m_JetError,m_UseWithJavascript,this->m_SessionId);
- }
Pour la rendre Asynchrone, nous allons simplement l’encapsuler dans une task, à l’aide des APIs de la PPL.
Code Snippet
- IAsyncOperation<JETWINRT::JetBlueTable^>^ JETWINRT::JetBlueDatabase::OpenTableAsync(Platform::String^ tablename,OpenTableFlags flags)
- {
- return create_async([this,tablename,flags]()
- {
- return OpenTable(tablename,flags);
- });
- }
Pour que la méthode soit considérée comme Asynchrone avec la WinRT, il faut suivre les règles suivantes :
- Tout d’abord, j’utilise une interface IAsyncOperation avec comme valeur de retour une instance de la classe JetBlueTable. Seul ce type d’interface est capable de traverser les frontières des langages.
- Afin de me simplifier la vie, j’utilise la méthode create_async de la PPL, qui est capable d’encapsuler une task et de retourner la bonne interface aux appelants.
- Afin de bien identifier les méthodes asynchrones, il est par convention, judicieux d’adjoindre à leur nom de méthode le mot clé Async.
Ensuite avec les “premises” (Javascript) ou les mots clés async/await de .NET il est très simple de les utiliser.
Code Snippet
- var table = await db.OpenTableAsync("TableTest", OpenTableFlags.ExclusiveLock);
La levée d’évènement et la synchronisation de thread
Comme je le disais plus haut, une autre manière de remonter de l’information à l’appelant est par l’intermédiaire d’évènement.
Par exemple dans mon composant, il est possible de s’abonner à l’évènement OnJetBlueError pour récupérer les messages d’erreurs.
L’évènement est déclaré de la manière suivante en C++/CX
Code Snippet
- event Windows::Foundation::EventHandler<Platform::String^>^ OnJetBlueError;
Ensuite pour déclencher l’évènement c’est simple, il suffit de faire appel à l’évènement directement
Code Snippet
- OnJetBlueError(this,stringRTErrorMessage);
Néanmoins, comme la majorité des appels de méthodes sont asynchrones, il sera préférable de fournir un mécanisme de synchronisation afin que l’appelant n’est pas explicitement à le faire lorsqu’il s’abonne à l’évènement.
Alors comment faire dans son composant ?
On va capturer le thread de principal de l’interface à l’aide de la classe CoreWindow
Code Snippet
- Windows::UI::Core::CoreWindow^ m_UIThread;
Néanmoins tel que, le compilateur émet un warning du type :
Warning C4451: 'JetBlue::WinRT::JetBlueError::m_UIThread' : Usage of ref class 'Windows::UI::Core::CoreWindow' inside this context can lead to invalid marshaling of object across contexts
Consider using 'Platform::Agile<Windows::UI::Core::CoreWindow>' insteadSans rentrer dans le détail, vous trouverez toutes les informations à la section Threading and Marshaling (C++/CX).
Ce warning provient du faite que CoreWindow n’est pas marqué comme Agile, ou pas avec le bon contexte de Threading.
Pour le rendre “Agile”, il faut passer par un objet Intermédiaire Platform::Agile<T>.Code Snippet
- Platform::Agile<Windows::UI::Core::CoreWindow^> m_UIThread;
Ensuite il faut utiliser la méthode statique GetForCurrentThread()
Code Snippet
- m_UIThread=Windows::UI::Core::CoreWindow::GetForCurrentThread();
Enfin, pour synchroniser les threads, nous allons utiliser la méthode RunAsync du Dispatcher, pour permettre de revenir dans le thread principal. A noter ici que la propriété HasThreadAccess, permet de savoir si nous sommes dans un autre thread. Dans le cas contraire ce n’est pas la peine de ce synchroniser.
Code Snippet
- void JetBlueError::NotifyError(std::wstring errormessage)
- {
- StringReference stringRTErrorMessage(errormessage.c_str());
- if (m_UIThread.Get() ==nullptr)
- {
- OnJetBlueError(this,stringRTErrorMessage);
- return;
- }
- if (m_UIThread.Get()->Dispatcher->HasThreadAccess) //I'm already on the UI Thread
- {
- OnJetBlueError(this,stringRTErrorMessage);
- }
- else
- {
- m_UIThread.Get()->Dispatcher->RunAsync(Windows::UI::Core::CoreDispatcherPriority::Normal,
- ref new Windows::UI::Core::DispatchedHandler([this,errormessage]()
- {
- StringReference s(errormessage.c_str());
- OnJetBlueError(this,s);
- }));
- }
- }
Eric
Ressources complémentaires :