Communication bidirectionnelle entre AutoCAD et un programme externe via COM

Un client m'a demandé si on pouvait faire dialoguer une application écrite en .NET avec un programme AutoLISP fonctionnant dans AutoCAD.

Dans le sens application .NET > AutoCAD, c'est très simple. Il suffit d'utiliser l'API COM d'AutoCAD :

try
{
    // On récupére une instance d'AutoCAD en cours d'exécution
    dynamic acad = Marshal.GetActiveObject("AutoCAD.Application");
    // On récupére le document actif
    dynamic activeDocument = acad.ActiveDocument;
    // On exécute une fonction AutoLISP via SendCommand
    activeDocument.SendCommand("(alert \"Coucou\") ");
}
catch (COMException ex)
{
    const uint MK_E_UNAVAILABLE = 0x800401e3;
    if ((uint)ex.ErrorCode == MK_E_UNAVAILABLE)
        MessageBox.Show("AutoCAD n'est pas en cours d'exécution.");
    else
        throw;
}

Dans cet exemple, j'exécute du code LISP à partir de mon application .NET via SendCommand par un simple clic sur un bouton. J'utilise la liaison tardive (late binding) pour ne pas avoir à me soucier de la version d'AutoCAD qui est en cours d'exécution.

Bouton qui permet de lancer la commande dans AutoCAD

Bouton qui permet de lancer la commande dans AutoCAD

Message affiché dans AutoCAD

Message affiché dans AutoCAD

Par contre, dans l'autre sens, AutoCAD/AutoLISP > application .NET, c'est beaucoup plus compliqué. Il faut implémenter un serveur COM out of process. Créer un serveur COM in process est relativement simple en .NET, mais le out of process demande un peu plus de travail. Heureusement, Microsoft fourni un exemple.

Voici en détail les différentes étapes nécessaires.

L'interface

Avec COM, il faut d'abord créer une interface :

    [Guid("DCC2B54F-8E40-4289-9C6D-35EE0F5F67A8")]
    [InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
    public interface IComServer
    {
        void WriteText(string text);
    }

On a une seule méthode qui va nous permettre d'écrire du texte. On ajoute 2 attributs pour définir le GUID et le type de l'interface.

L'implémentation de l'interface

J'ai d'abord modifié Program.cs de façon à pouvoir obtenir une référence à mon formulaire principal :

    static class Program
    {
        private static MainForm mainForm;
 
        public static MainForm MainForm
        {
            get { return mainForm; }
        }
 
        /// <summary>
        /// Point d'entrée principal de l'application.
        /// </summary>
        [STAThread]
        private static void Main()
        {
            [..]
            mainForm = new MainForm();
            Application.Run(MainForm);
        }
    }

Ensuite, j'ai ajouté une méthode WriteText à ma classe MainForm:

    public void WriteText(string text)
    {
        textBox1.BeginInvoke((MethodInvoker)(() 
            => textBox1.AppendText(text + Environment.NewLine)));
    }

Comme l'objet COM est instancié à partir d'un fil d'exécution (thread) différent de celui de l'interface, il faut utiliser Invoke ou BeginInvoke sinon on obtient une exception. Dans mon exemple j'utilise BeginInvoke puisque je n'ai pas besoin d'un appel bloquant.

Puis enfin j'ai implémenté mon objet COM en le dérivant de mon interface.

    [ClassInterface(ClassInterfaceType.None)]
    [Guid("D3714007-CDA9-4663-88E4-0FD13C19D93E"), ComVisible(true)]
    class ComServer: IComServer
    {
        public void WriteText(string text)
        {
            Program.MainForm.WriteText(text);
        }
    }

Dans cet exemple, j'ai privilégié la simplicité. Mais dans un code de production, il faudrait injecter la dépendance à Program.MainForm.

Notez également les différents attributs nécessaires pour que mon objet soit visible via COM.

Enregistrement de notre serveur dans la base de registre

Pour enregistrer notre serveur, on utilise regasm. Dans le post-build de mon projet Visual Studio, j'ajoute donc cette ligne de commande :

C:\Windows\Microsoft.NET\Framework\v2.0.50727\regasm.exe "$(TargetPath)"

Si vous utilisez .NET 4.0, il faut utiliser la version de regasm livré avec cette version du framework.

C:\Windows\Microsoft.NET\Framework\v4.0.30319\RegAsm.exe "$(TargetPath)"

Notez que je compile en x86, car regasm se mélange les pinceaux avec AnyCPU. J'utilise donc ici les versions 32 bits de regasm.

Ensuite, on doit implémenter 2 méthodes pour enregistrer et supprimer l'enregistrement de notre objet car regasm ne sait enregistrer que des serveurs COM in process. On doit donc supprimer la clé InprocServer32 qu'il crée et la remplacer par une clé LocalServer32.

    class ComServer: IComServer
    {
        [..]
 
        [EditorBrowsable(EditorBrowsableState.Never)]
        [ComRegisterFunction]
        public static void Register(Type t)
        {
            try
            {
                COMHelper.RegasmRegisterLocalServer(t);
            }
            catch (Exception ex)
            {
                Trace.WriteLine(ex.Message);
                throw;
            }
        }
 
        [EditorBrowsable(EditorBrowsableState.Never)]
        [ComUnregisterFunction]
        public static void Unregister(Type t)
        {
            try
            {
                COMHelper.RegasmUnregisterLocalServer(t);
            }
            catch (Exception ex)
            {
                Trace.WriteLine(ex.Message);
                throw;
            }
        }
    }

Ici j'utilise COMHelper qui est une classe fournie par Microsoft :

    using System;
    using Microsoft.Win32;
    using System.Reflection;
 
    internal class COMHelper
    {
        /// <summary>
        /// Register the component as a local server.
        /// </summary>
        /// <param name="t"></param>
        public static void RegasmRegisterLocalServer(Type t)
        {
            GuardNullType(t, "t");  // Check the argument
 
            // Open the CLSID key of the component.
            string path = @"CLSID\" + t.GUID.ToString("B");
            using (RegistryKey keyCLSID = Registry.ClassesRoot.OpenSubKey(path, true))
            {
                if (null == keyCLSID) 
                    throw new ApplicationException(string.Format(
                        "Can not open the registry key {0}", path));
 
                // Remove the auto-generated InprocServer32 key after registration
                // (REGASM puts it there but we are going out-of-proc).
                keyCLSID.DeleteSubKeyTree("InprocServer32");
 
                // Create "LocalServer32" under the CLSID key
                using (RegistryKey subkey = keyCLSID.CreateSubKey("LocalServer32"))
                {
                    if (null == subkey) 
                        throw new ApplicationException(
                            "Can not create the registry key LocalServer32");
                    subkey.SetValue("", Assembly.GetExecutingAssembly().Location,
                        RegistryValueKind.String);
                }
            }
        }
 
        /// <summary>
        /// Unregister the component.
        /// </summary>
        /// <param name="t"></param>
        public static void RegasmUnregisterLocalServer(Type t)
        {
            GuardNullType(t, "t");  // Check the argument
 
            // Delete the CLSID key of the component
            Registry.ClassesRoot.DeleteSubKeyTree(@"CLSID\" + t.GUID.ToString("B"));
        }
 
        private static void GuardNullType(Type t, String param)
        {
            if (null == t)
                throw new ArgumentNullException(param);
        }
    }

Ces 2 méthodes créent et suppriment les clés de la base de registre qui sont nécessaires pour l'enregistrement de notre serveur COM.

Code AutoLISP

Pour finir voici le code AutoLISP qui permet d'envoyer du texte à l'application :

    (defun exec-cmd-in-external-app (/ my-prog)
        ;; Charge les extensions COM Visual LISP
        (vl-load-com)
         ;; Récupère l'instance de mon programme en cours d'exécution
        (setq my-prog (vlax-get-object "CommunicationBidirectionnelle.COMServer"))
         ;; Appel de la méthode WriteText
        (vlax-invoke-method my-prog 'WriteText 
            (strcat "Texte envoyé depuis AutoCAD (" (getvar "DWGNAME") ")")
         )
        (princ)
    )

Ça marche avec AutoLISP, mais on peut bien sur utiliser n'importe quel langage qui supporte COM comme VBScript par exemple.

Etiquettes:

Ajouter un commentaire