Bonjour à tous,
Cette semaine, j'ai choisi un sujet "plus léger": comment préparer des valeurs de retour depuis DotNet. Il existe quelques réponses simples à des cas simples mais lorsque vient le temps, par exemple, de retourner une liste complexe ou un jeu de sélection (selection set), les exemples manquent. Le but de cet article est de vous fournir un exemple des cas les plus communs:
Pour commencer, vous trouverez la solution (fichier *.sln) ainsi que son contenu dans un fichier Zip à l'adresse suivante: Chargement de la solution. Celle-ci comporte une série de fonctions pour AutoLISP qui génèrent différentes valeurs de retour selon des valeurs aléatoires (afin de rendre la solution la plus générale possible). En plus de la solution, vous y trouverez un fichier nommé "Liste des exemples disponibles.doc" qui montre l'utilisation et des exemples.
Vous trouverez également un exemple permettant d’optimiser le chargement du fichier (bien que celui-ci soit très modeste) via l’interface IExtensionApplication.
Liste des exemples disponibles :
Note: afin que vous puissiez ouvrir la solution, elle a été écrite avec Visual Studio 2005 (VS2005). Vous aurez probablement à modifier les chemins des références (acdbmgd.dll et acmgd.dll) si vous passez à VS2008. En plus, si vous migrez pour AutoCAD 2013 (VS2010), vous aurez à migrer d'autres éléments tels que spécifiés dans l'article sur la migration 2013 (ajout de accoremgd.dll, passage au FrameWork, etc.).
Commenter

Commentaire de Serge Camiré le 5 juin 2012 à 0:32 Salut Gile
Je ne l'ai pas dit mais je ne raffole pas du C#. La raison en est que je n'ai pas assez travaillé avec et je me cherche encore. Pourtant, je n'ai pas de problème avec le C++. C'est vraiment un état d'esprit. En fait, pourquoi avoir choisi le VB. C'est très simple: on avait plusieurs projets développés depuis plusieurs années en VBA et en VB6. Le VB7 devenait une extension naturelle (quoi que la migration ne s'est pas déroulée comme quand on passait de VB4 à VB5 puis à VB6). Heureusement qu'on connaissait bien le C++, ce qui nous a fait comprendre les concepts plus vite.
Autre chose que j'aime du VB, et c'est peut-être parce que je ne connais pas assez le C# pour savoir que ce n'est pas un problème, c'est que dès qu'on fait une erreur de syntaxe, l'IDE la détecte tout de suite et nous suggère quelque chose. Dès qu'on fait les corrections, elles disparaissent de la fenêtre des erreurs. Avec C#, elles persistent tant qu'on n'a pas régénérée la solution. Il y a aussi le formatage du texte qui est plus simple (en fait, qui est inexistant sous C# et C++). C'est pourquoi j'ai acheté Visual Assist X de Whole Tomato Software
Sinon, les quelques petites différences qui avantageaient parfois l'un, parfois l'autre sont censées être complètement disparues avec VS 2010.
Pour F#, c'est vrai que le runtime n'était pas installé jusqu'au FW 3.5 mais je crois qu'il fait partie intégrante du FW4.0

Commentaire de gile le 4 juin 2012 à 23:52 Je crains de faire un peu partie de ceux qui n'aiment pas le VB...
J'utilise F# plutôt à titre expérimental pour le moment. Ce langage est encore assez confidentiel, à fortiori chez les programmeurs AutoCAD. Il a aussi d'autres défauts : le runtime F# n'est pas automatiquement installé avec le Framework .NET (déploiement moins aisé) et ne propose pas d'éditeur de formulaires. Mais j'espère bien un jour pouvoir combiner des projets F# et C# dans une même application.
Ce qui m'a séduit avec F#, c'est la programmation fonctionnelle, la syntaxe concise et déclarative et la possibilité de tester des expressions directement dans la fenêtre interactive (un peu comme la console Visual LISP) ainsi que ses capacités en POO.
À part ça, ce langage me semble plus adapté pour tout ce qui relève plus spécifiquement des mathématiques et du traitement des collections, encore qu'avec Linq on puisse faire presque l'équivalent. Sinon, il est aussi en avance sur ses grands frères en ce qui concerne la programmation asynchrone et parallèle, mais je n'ai encore rien fait de sérieux en ce sens.
Voir cette vidéo (présentation jubilatoire de F#) :
http://channel9.msdn.com/Blogs/pdc2008/TL11
et/ou celle là (consacrée à F# avec AutoCAD) :
http://www.acadnetwork.com/topic-77.0.html

Commentaire de Serge Camiré le 3 juin 2012 à 23:14 La liaison tardive a ses avantages et inconvénients. Je viens de terminer un développement pour SolidWorks en VBA et tout se passait en COM. C'est vraiment ch... parce que doit faire preuve d'une foi aveugle quand on travaille car on n'a pas d'Intellisense, on ne sait même pas si ce qu'on tape existe tant qu'on n'exécute pas le code et on le découvre avec des erreurs de run-time. Lorsqu'on place des points d'arrêt, il est très difficile de pouvoir consulter l'état des variables. En revanche, si tout fonctionne, cela nous permet de faire des connexion a des mondes inconnus du compilateur.
Tout les langages de script sont également dépourvus de types forts, ce qui est un peu ennuyeux.
Ce sur quoi je m’interrogeais était à savoir si on on utilisait une signature de fonction qui n'était pas typée mais dont tout le processus l'était puis qu'on appliquait un cast "dégradant" à la toute fin. En fait, je me pose la question mais l'idée commence à me plaire. Il faut juste attendre l'abandon complète de la version 2009 pour avoir le plein support du type Object.
En passant, VBA (et VB6) était des langages dont le fonctionnement était orienté sur la gestion d'erreur, comme si l'erreur faisait partie de la normalité (on error resume next). Les gens qui enseignent la programmation que je connais pestent tous contre cette façon de faire (faut dire que ce sont les mêmes qui détestent VB). De la même manière, on peut abuser des Try Catch. Selon eux, il vaut mieux tester les situations prévisibles de conflit et de bâtir les solutions en conséquence.
Je vois que tu utilises le F#. Est-ce à titre expérimental ou as-tu trouvé une situation où ce langage est supérieur ?

Commentaire de gile le 3 juin 2012 à 22:23 Sans faire le "puriste", une expérience récente de migration de code VBA en C# m'a fait clairement voir que la liaison tardive (typage dynamique) dont use (voire abuse) VBA (et dans une moindre mesure VB) ne facilite pas le débogage et la maintenance dans un environnement au typage fort.
En F#, par exemple, où le typage est encore plus strict qu'en C# (aucune conversion implicite) la question d'utiliser le type Object ne se pose même pas (à moins de faire un boxing explicite de la valeur de retour ce qui n'a pas vraiment d'intérêt tans du point de vue de la performance que de la lisibilité du code.
Exemple avec la fonction acos (comme ci dessous), la signature de arcCos est : ResultBuffer -> double, en cas d'erreur d'argument, une boite d'alerte affiche le message d'erreur et l'exception est relancée.

Commentaire de Serge Camiré le 3 juin 2012 à 16:36 Salut,
Je peux confirmer: l'erreur ne se produit qu'avec VS2005 (AutoCAD 2007-2009).
Dans la mesure où l'on souhaite que le code source soit compatible avec toutes les versions de Visual Studio, cela nous force à mettre des directives de compilation conditionnelle (#if). Ce n'est pas très joli au niveau de la déclaration même de la fonction. On pourrait toujours plaider que l'on n'a plus à supporter les vieilles versions mais là où j'ai un gros mandat (au Ministère des Transports), les choses ne vont pas vite. Encore aussi qu'il faudrait accepter d'avoir des valeurs de retour non typé via Object, ce qui nous déplaît un peu à tout les 2.
Je sais qu'on a discuté du typage dynamique et de ses avantages. Faut-il s'obstiner à être puriste ? Bonne question.

Commentaire de gile le 3 juin 2012 à 15:17 J'ai testé en C# et VB.
Avec quelle version d'AutoCAD as-tu l'erreur ?

Commentaire de Serge Camiré le 3 juin 2012 à 15:08 C'est peut-être que j'ai testé avec VB. Je vais regarder cela en C#

Commentaire de gile le 3 juin 2012 à 15:04 C'est curieux, je n'ai pas l'erreur que tu décris avec un type de retour Object (testé sur A2007, A2010, A2013)

Commentaire de Serge Camiré le 3 juin 2012 à 13:59 Merci Gile,
Est-ce Tony qui se cache derrière l'avatar? Ça expliquerait bien des choses.
Pour en revenir à notre discussion, je n'avais jamais essayé de retourner un type fort autre que TypedValue, ResultBuffer ou Object. La raison en est probablement mon héritage du C++. Avec ObjectArx, on retourne toujours directement un code de statut (succès ou erreur) et, indirectement, on envoie la valeur de retour via un ResultBuffer (il n'y a pas de TypedValue).
Exemple:
Adesk::IntPtr testInteger(struct resbuf *pTmpBuf)
{
if (pTmpBuf == NULL) {
struct resbuf* rbuf = acutBuildList(RTSHORT, 123, RTNONE); // On retourne 123 pour simplifier l'exemple.
acedRetVal(rbuf);
}
else {
acdbFail(_T("Erreur -- Trop de Paramètres!\nConsultez le fichier d'aide.\n"));
acedRetNil();
}
return RTNORM;
}
Ceci étant dit, ça m’apparaît vraiment limitatif de retourner les autres types. Ce que je voulais dire en mentionnant le mauvais usage du type Object, c'est que lorsqu'il sert pour substituer un ResultBuffer, on peut retourner soit une liste (ou un cons) ou soit nil. Si on déclare ResultBuffer, ça retourne une liste avec nil à l'interieur (e.g. '(nil)) et c'est un peu moins beau mais au moins, on sait exactement à quoi s'attendre.
Toujours si on utilise Object comme type de retour, et qu'on passe autre chose qu'un ResultBuffer ou Nothing (VB) ou null (C#), on obtient une erreur.
Exemple:
<LispFunction("exemple_int")> _
Public Function exemple_int(ByVal myLispArgs As ResultBuffer) As Object
Return 5
End Function
Dans AutoCAD
Commande: (exemple_int)
System.InvalidCastException: Le cast spécifié n'est pas valide.
à Autodesk.AutoCAD.Runtime.CommandClass.InvokeWorker(MethodInfo mi, Object
commandObject, Boolean bLispFunction)
à
Autodesk.AutoCAD.Runtime.CommandClass.InvokeWorkerWithExceptionFilter(MethodInfo
mi, Object commandObject, Boolean bLispFunction)
à Autodesk.AutoCAD.Runtime.PerDocumentCommandClass.Invoke(MethodInfo mi,
Boolean bLispFunction)
à Autodesk.AutoCAD.Runtime.CommandClass.CommandThunk.InvokeLisp(); erreur:
Demande ADS erronée
Pour ce qui est du cons, une petite précision:le type de retour de la fonction doit être un TypedValue. Ceci est quand même très intéressant car ça élargit les possibilités de retour. Si j'avais choisi un ResultBuffer, c'est en analysant la structure des informations en entrée (le ByVal myLispArgs As ResultBuffer). C'est là que j'ai vu qu'AutoCAD présentatit la chose en 4 éléments (ListBegin, Val1, Val2, DottedPair) plutôt qu'en 5 (ListBegin, Val1, DottedPair, Val2, ListEnd) comme on se s'y aurait attendu lorsqu'on est habitué en AutoLISP. Pour les point2d et point3d, c'était déjà dans l'exemple.
Exemple:
<LispFunction("exemple_cons2")> _
Public Function exemple_cons2(ByVal myLispArgs As ResultBuffer) As TypedValue
Return New TypedValue(1, "Texte")
End Function
Pour la gestion des erreurs, je n'ai aucune difficulté à l'idée de lancer des exceptions. C'est ce que je faisais à l'époque. Mais avec le temps, je me suis rendu compte que cela faisait peur aux utilisateurs. Maintenant, j'utilise une boite de dialogue personnalisée avec des instructions aux utilisateurs pour nous rapporter l'erreur ainsi que des assertions.
Ça fait du bien de revenir dans un monde civilisé :-)

Commentaire de gile le 3 juin 2012 à 11:26 Salut,
J'espère ne pas avoir l’arrogance de Tony Tanzillo (même si je rêverais d'avoir ses connaissances).
Je vais essayer de préciser mon propos.
Ce avec quoi je ne suis pas tout à fait d'accord, c'est quand tu dis, dans le fichier "Liste des exemples.doc" :
"Attention : DotNet ne peut retourner qu’un type fortement typé soit TypedValue ou ResultBuffer. Vouloir définir une valeur de type Objet fera planter AutoCAD (sauf si la valeur de retour est explicitement Nothing, ce qui est peu pratique)."
D'après ma (petite) expérience, on peut utiliser directement comme valeur de retour les types forts compatibles avec les types AutoLISP :
- Int16 et Int32 (INT)
- String (STR)
- Double ou Float (REAL)
- ObjectId (ENAME)
- SelectionSet (PICKSET)
- ResultBuffer (LIST)
- Point2d, Point3d (LIST)
Ou, pour les atoms et les points, une instance de TypedValue qui "emballe" le type fort. Mais on peut aussi, dans tous les cas utiliser le type Object. J'essaye, de mon côté, d'éviter cette dernière solution, autant j'aime la souplesse du typage dynamique en LISP autant je préfère privilégier la rigueur du typage fort en .NET.
Si la fonction LISP doit pouvoir retourner nil, pour les types non nullables (Int16, Int32, Double, ObjectId, Point2d, Point3d) on sera obligé d'utiliser le type TypedValue (avec TypedValue(LispDataType.Nil)) ou le type Object (avec null ou Nothing).
Pour les atoms de type nullable (String, SelectionSet), on peut utiliser directement le type avec null/Nothing comme valeur de retour pour nil ou, bien sûr, le type TypedValue.
Quand le type de retour est ResultBuffer (LIST AutoLISP), on n'est pas obligé de retourner un ResultBuffer contenant TypedValue(LispDataType.Nil) - '(nil), on peut utiliser null/Nothing pour que la fonction LISP retourne simplement nil (un ResultBuffer vide génère une erreur).
En ce qui concerne la gestion des erreurs pour le nombre, le type et la valeur des arguments d'une fonction LISP, on peut faire retourner nil à la fonction. De mon côté, dans un souci de cohérence, je préfère générer une erreur comme le font les fonctions LISP natives. Les fonctions LISP définies en .NET sont destinées à des programmeurs LISP et je pense que c'est au programme(ur) LISP de gérer ce type d'erreur.
Le problème est que je ne sais pas (et ça n'est peut-être pas possible) retourner le message d'erreur dans l'éditeur Visual LISP. L'erreur est "masquée" par : "Demande ADS erronée" et on ne peut lire le message de la classe dérivée qu'en début du StackTrace qui s'inscrit dans la fenêtre de texte (encore une fois, ça ne concerne que le programmeur quand il débogue une routine qui utilise la fonction).
Dernier point, quand la fonction doit retourner une paire pointée (cons) qui respecte les codes de groupe DXF, il n'est pas nécessaire de construire un ResultBuffer qui contient la paire pointée, on peut retourner directement une instance de TypedValue :
return new TypedValue(1, "texte");
au lieu de :
return new ResultBuffer(
new TypedValue((int)LispDataType.ListBegin),
new TypedValue((int)LispDataType.Int16, 1),
new TypedValue((int)LispDataType.Text, "texte"),
new TypedValue((int) LispDataType.DottedPair));
Ça vaut aussi pour les points :
return new TypedValue(10, Point3d.Origin); => (10 0.0 0.0 0.0)
En conclusion, je suis globalement d'accord avec ton article, pour les valeurs de retour des fonctions LISP, je préfère privilégier les types TypedValue et ResultBuffer. Je voulais juste partager ce que j'ai pu découvrir, tout seul ou grâce à d'autres, pour préciser quelques détails.
© 2013 Créé par AUGIfr
Vous devez être membre de AUGIfr pour ajouter des commentaires !
Rejoindre AUGIfr