Breathing fresh life into LISP applications with a modern GUI

This recent entry on Jimmy Bergmark's JTB World Blog brought to my attention the fact that ObjectDCL is about to become an Open Source project. Chad Wanless, the father of ObjectDCL, was a very active ADN member for many years, but - according to this post on the ObjectARX discussion group - is now unable to spend time working on ObjectDCL due to a severe medical condition. In case Chad is reading this... Chad - all of us here at ADN wish you a speedy recovery and all the best for your future endeavours.

Ignoring the background behind the decision to post the technology as Open Source, this is good news for developers with legacy codebases that include user interfaces implemented using DCL (Dialog Control Language).

I did want to talk a little about what other options are available to developers of LISP applications that make use of DCL today. There are a couple of approaches to calling modules implementing new user interfaces for LISP apps, whether through COM or through .NET. I'll talk about both, but I will say that .NET is the most future-proof choice at this stage.

Both techniques work on the principle that you redesign your user interface using VB6 or VB.NET/C#, and call through to these functions from LISP. Reality is often more complex - you may have more complex interactions from (for instance) particular buttons in your dialog - but these examples demonstrate what you can do to replace a fairly simple UI where you pass the initial variables into a function and receive the modified variables at the other end, once the user closes the dialog. You can also extend it to handle more complex situations, but there may be much more work needed - perhaps even use of AutoCAD's managed API from within the dialog code.

COM: Using a VB ActiveX DLL from Visual LISP

For an in-depth description of this technique, ADN members can find the information here. I apologise to those who are not able to access this content, but I don't want to dillute the issue by copying/pasting the whole article into this blog. Especially as this technique is describing the use of VB6, which is no longer at the forefront of Microsoft's development efforts.

The approach is to create an ActiveX DLL project in VB6, which is simply a COM module implementing code that can be referenced and called using a ProgID. AutoCAD's COM Automation interface exposes a method called GetInterfaceObject from the Application object that simply calls the equivalent of CreateObject on the ProgID passed in, but within AutoCAD's memory space. Once you've loaded a module using GetInterfaceObject, not only can you then call code displaying fancy VB-generated UIs from LISP, but because the code is in the same memory space as AutoCAD, it executes very quickly - on a par with VBA, ObjectARX or the managed API in terms of the speed with which it can access AutoCAD's object model.

.NET: Defining LISP-callable functions from a .NET application

The following technique is really the more future-proof approach, and has become possible since the implementation of "LISP callable wrappers" in AutoCAD 2007's managed API. Essentially it comes down to the ability to declare specific .NET functions as being LISP-callable. If you look back at one of my early posts about creating a .NET application, you'll notice the use of an attribute to declare a command, such as <CommandMethod("MyCommand")>. With AutoCAD 2007 you can simply use <LispFunction("MyLispFunction")> to denote a function that can be called directly from LISP.

From there it's simply a matter of unpackaging the arguments passed in and packaging up the results (the bit in-between is where you get to have fun, using .NET capabilities to create beautiful user interfaces or to integrate with other systems etc., etc.). Here's some code to show the handling of arguments and packaging of the results:

<LispFunction("LISPfunction")> _
Public Function VBfunction(ByVal rbfArgs As ResultBuffer) As ResultBuffer

    'Get the arguments passed in...
    Dim arInputArgs As Array
    Dim realArg1 As Double
    Dim intArg2 As Integer
    Dim strArg3 As String
    arInputArgs = rbfArgs.AsArray
    realArg1 = CType(arInputArgs.GetValue(0), TypedValue).Value
    intArg2 = CType(arInputArgs.GetValue(1), TypedValue).Value
    strArg3 = CType(arInputArgs.GetValue(2), TypedValue).Value

    'Do something interesting here...
    '...
    '...

    'Package the results...
    'Use RTREAL (5001) for doubles
    'Use RTSTR (5003) for strings
    'Use RTSHORT (5005) for integers
    Dim rbfResult As ResultBuffer
    rbfResult = New ResultBuffer( _
        New TypedValue(CInt(5001), 3.14159), _
        New TypedValue(CInt(5003), 42), _
        New TypedValue(CInt(5005), "Goodbye!"))
    Return rbfResult

End Function

The code assumes the first argument passed in will be a real, followed by an integer and then finally a string. Here's what happens if you call the code like this:

Command: (LISPfunction 1.234 9876 "Hello!")
(3.14159 42 "Goodbye!")

Here are a couple of useful articles on the ADN site regarding this:

LispFunction examples for AutoLISP to .NET
.NET ResultBuffer returns dotted pairs to Visual LISP instead of normal list

16 responses to “Breathing fresh life into LISP applications with a modern GUI”

  1. only works, if no separate Namespace.
    How to Import des LispFunction in a VLX, build with separate Namespace ?

  2. I have no idea: I haven't had to do this, myself. Please post your question to the ADN team, if you're a member, or to one of our online discussion groups, otherwise.

    Regards,

    Kean

  3. Why I can't call a dll activex (VB6) in Autocad 2009 64 bit, its run ok in AutoCAD 2009 win 32.

    (setq acadApp (vlax-get-acad-object))
    (setq MyApp (vla-GetInterfaceObject acadApp "myLib.Mycls"))

  4. I'm no 64-bit expert - I'm still working on a 32-bit OS - but I imagine you have a 32-bit component (built using VB6) that you're trying to load into a 64-bit process.

    You'll probably need to rebuild your VB6 for a 64-bit system.

    But either ADN or someone on the AutoCAD .NET Discussion Group will know more.

    Kean

  5. Thanks for the answer, yes, I rebuild it in VB.net for 32 and 64 but but it doesnt works.

  6. Hi Kean,
    I am getting ready to start what will likely be a long project, sorting through my companies Lisp directory and determining what is useful and what is not.(there are almost 500 files in there that date back to 1986, and though I have been here for 11 years, even I don't know what all of them do.)

    I figure while I am doing that, I will likely find some things I would transfer into one of our .NET projects as LispFunctions and I was wondering... I've looked everywhere I can think to look, and I don't have any reference that tells me where you got those 5000 series TypeCodes. Actually, I can't find any list of type codes that goes beyond the 1000 series Xdata codes.

    Beyond knowing where to find them, I was also wondering the significance of using them as opposed to the standard set of codes.

  7. Hi David,

    If you install any ObjectARX SDK you will find them defined in the adscodes.h header in the inc folder:

    /* Result value type codes

    These are used in the resbuf. These ARE NOT the same
    as the group codes used in the entity access routines, and should
    not be confused with those. The same result buffer IS used for
    entity records, however, in which case the restype field may take
    on many more values than those listed here.
    */
    #define RTNONE 5000 /* No result */
    #define RTREAL 5001 /*Real number */
    #define RTPOINT 5002 /* 2D point X and Y only */
    #define RTSHORT 5003 /* Short integer */
    #define RTANG 5004 /* Angle */
    #define RTSTR 5005 /* String */
    #define RTENAME 5006 /* Entity name */
    #define RTPICKS 5007 /* Pick set */
    #define RTORINT 5008 /* Orientation */
    #define RT3DPOINT 5009 /* 3D point - X, Y, and Z */
    #define RTLONG 5010 /* Long integer */
    #define RTVOID 5014 /* Blank symbol */
    #define RTLB 5016 /* list begin */
    #define RTLE 5017 /* list end */
    #define RTDOTE 5018 /* dotted pair */
    #define RTNIL 5019 /* nil */
    #define RTDXF0 5020 /* DXF code 0 for ads_buildlist only */
    #define RTT 5021 /* T atom */
    #define RTRESBUF 5023 /* resbuf */
    #define RTMODELESS 5027 /* interrupted by modeless dialog */

    These definitions are basically the ones that are needed in this circumstances (when passing data via ResultBuffers).

    Regards,

    Kean

  8. Thanks Kean. Yesterday, when I looked at the ARX Managed Class Reference page for the ResultBuffer class, it said "See ObjectARX Global Utility Functions->Variables, Types, and Values Defined in ObjectARX->Result Buffers and Type Codes in the ObjectARX Developer's Guide"

    but under ObjectARX Developer's Guide, there is no ObjectARX Global Utility Functions entry.

    I finally found it, under Arx Dev Guide->Basic Interaction with AutoCAD->ObjectARX Global Utility Functions->...

  9. Good that you found it. I tend to work from first principles for these things, so sometimes forget intermediate documentation exists.

    Kean

  10. Hello Kean,
    I currently use "objAcadDoc.SendCommand(stringLISP)" or
    "objDoc.SendStringToExecute("_commandLISP ", true, false, false)" to register an lisp expression. But the first way use COM (is slow) and the second way is Asyncrono (only execute in the end of command).
    How can I send to AutoCAD a lisp expression, without using COM or ASYNC metodes?

  11. Kean Walmsley Avatar

    Hello Vitor,

    These are the two recommended ways - I tend to use SendStringToExecute() with a continuation command.

    But please do ask ADN - someone in my team may have a better approach for you.

    Regards,

    Kean

  12. Then ,how to get return args when call lispfunction in .net ,like,
    var app=new AutoCAD.AcadApplication();
    var doc=app.Documents.Item(0);
    doc.SendCommand(cmd);
    //suppose cmd netload a lispfunction and call it

    I noticed that sendcommand return type is void ,so ,how can I get the lispfunction return arg?

    1. Commands don't return anything. The LispFunction() attribute allows .NET functions to be called from LISP. You seem to be calling something (LISP?) from .NET... maybe look at Application.Invoke(), instead.

      Kean

      1. I wrote a AutoCAD CSharp plug-in project and I use this :<lispfunction("mylispfunction")> attribute wrote a function.
        Then I netload this project's dll file in another project ,and I also wanna call the function and get its return arg.
        Sorry If I don't make myself clear.

        1. This is getting off topic: it sounds as though you want to share functionality between two .NET modules. If that's the case, there are better ways to approach this than via LISP.

          Please post a complete description of what you're trying to do to the AutoCAD .NET Discussion Group:

          forums.autodesk.com/

          Kean

          1. Thank you for your patience

Leave a Reply to Kean Walmsley Cancel reply

Your email address will not be published. Required fields are marked *