.NET
cancel
Showing results for 
Show  only  | Search instead for 
Did you mean: 

How to cleanly throw an exception from LispFunction

9 REPLIES 9
Reply
Message 1 of 10
_gile
1353 Views, 9 Replies

How to cleanly throw an exception from LispFunction

Hi,

 

When defining new LISP function with .NET, I think it's necessary to first check for the argument validity (number and type) and I am convinced the evaluation must stop in case of invalid inputs (as the built-in LISP function work).

 

Fot this, I use some classes which inherit from System.Exception:

 

using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Runtime;

namespace LispFunctionSample
{
    /// <summary>
    /// Base exception type for LISP exceptions.
    /// </summary>
    class LispException : System.Exception
    {
        /// <summary>
        /// Creates a new instance of LispException.
        /// </summary>
        /// <param name="msg">The message to be displayed when LispException is thrown.</param>
        public LispException(string msg)
            : base(msg) { }
    }

    /// <summary>
    /// Exception to be used if the LISP function is called with too few arguments.
    /// </summary>
    class TooFewArgsException : LispException
    {
        /// <summary>
        /// Creates a new instance of TooFewArgsException.
        /// </summary>
        public TooFewArgsException()
            : base("too few arguments") { }
    }

    /// <summary>
    /// Exception to be used if the LISP function is called with too many arguments.
    /// </summary>
    class TooManyArgsException : LispException
    {
        /// <summary>
        /// Creates a new instance of TooManyArgsException.
        /// </summary>
        public TooManyArgsException()
            : base("too many arguments") { }
    }

    /// <summary>
    /// Exception to be used if the LISP function is called with an incorrect argument type.
    /// </summary>
    class ArgumentTypeException : LispException
    {
        /// <summary>
        /// Creates a new instance of ArgumentTypeException
        /// </summary>
        /// <param name="msg">The string to be displayed in the message 
        /// (fixnump, numberp stringp, listp, lentityp, lselsetp).</param>
        /// <param name="tv">The TypedValue representing the incorrect argument.</param>
        public ArgumentTypeException(string msg, TypedValue tv)
            : base(string.Format(
            "bad argument type: {0}: {1}",
            msg, 
            tv.TypeCode == (int)LispDataType.Nil ? 
            "nil" :  
            tv.TypeCode == (int)LispDataType.Text ? 
            "\"" + tv.Value + "\"" : 
            tv.Value))
        { }
    }
}

 

These exceptions may be thrown from the LispFunction code, catched to display a message and re-raised to stop the LISP evaluation.

 

Example:

        [LispFunction("acos")]
        public double Acos(ResultBuffer resbuf)
        {
            try
            {
                if (resbuf == null)
                    throw new TooFewArgsException();
                TypedValue[] args = resbuf.AsArray();
                if (args.Length > 1)
                    throw new TooManyArgsException();
                int code = args[0].TypeCode;
                if (code != (int)LispDataType.Int16 && code != (int)LispDataType.Int32 && code != (int)LispDataType.Double)
                    throw new ArgumentTypeException("numberp", args[0]);
                double value = Convert.ToDouble(args[0].Value);
                if (value < -1.0 || value > 1.0)
                    throw new LispException("the value must be between -1.0 and 1.0 inclusive");
                return Math.Acos(value);
            }
            catch (System.Exception ex)
            {
                AcAp.DocumentManager.MdiActiveDocument.Editor.WriteMessage("\n; error: " + ex.Message + "\n");
                throw;
            }
        }

 

This works quite fine except the message in the command line is followed by an eInvalidAdsName stack trace and, in the Visual LISP console, the message is: "; error: ADS request error".

 

So, is there a way to avoid the eInvalidAdsName stack trace in the command line and have the thrown exception message in the Visual LISP console so that it mimics closer the built-in functions?

 

Thanks in advance.

 

 



Gilles Chanteau
Programmation AutoCAD LISP/.NET
GileCAD
GitHub

9 REPLIES 9
Message 2 of 10
DiningPhilosopher
in reply to: _gile

Hi Giles.

 

The short answer is no.

 

Managed exceptions (that are not signaled by hardware or the OS), like those you are throwing, are being caught by the part of the managed runtime that invokes your LispFunction.  The caught exception is handled, the Exception's Message property is displayed on the console, and an RTXXXX result code is returned to AutoCAD, just like most externally-defined LISP written in C++ and registered using acedDefun()/ads_regfun() typically do.  It's that result code that is what causes AutoCAD to display the generic error message.  Note that in ObjectARX, there is no way to convey a more-specific error message, and hence, the only way to do it is to write it to the console yourself, just before raising the exception.

 

The commuication between LISP and AutoCAD is archaic, and provides no way to convey specifics of an error, other than by writing it to the console yourself.

 

 

 

 

Message 3 of 10
_gile
in reply to: DiningPhilosopher

Hi Tony,

 

Thank you for the quick and clear answer, this is what I feared.

I will continue to use alert box to warn the user in case of error.



Gilles Chanteau
Programmation AutoCAD LISP/.NET
GileCAD
GitHub

Message 4 of 10
FRFR1426
in reply to: _gile

As you can not cleanly throw an exception from AutoLISP, you should define a convention for returning an error and write a wrapper on the AutoLISP side to handle these errors.

 

First rewrite your .NET code like this :

 

[LispFunction("_acos")]
public ResultBuffer Acos(ResultBuffer resbuf)
{
    try
    {
        if (resbuf == null)
            throw new TooFewArgsException();
        TypedValue[] args = resbuf.AsArray();
        if (args.Length > 1)
            throw new TooManyArgsException();
        int code = args[0].TypeCode;
        if (code != (int)LispDataType.Int16 && code != (int)LispDataType.Int32 && code != (int)LispDataType.Double)
            throw new ArgumentTypeException("numberp", args[0]);
        double value = Convert.ToDouble(args[0].Value);
        if (value < -1.0 || value > 1.0)
            throw new LispException("the value must be between -1.0 and 1.0 inclusive");
        return new ResultBuffer(new[]
            {
                new TypedValue((int) LispDataType.T_atom), 
                new TypedValue((int) LispDataType.DottedPair),
                new TypedValue((int) LispDataType.Double, Math.Acos(value)), 
            });
    }
    catch (System.Exception ex)
    {
        Application.DocumentManager.MdiActiveDocument.Editor.WriteMessage("\n; error: " + ex.Message + "\n");
        return new ResultBuffer(new[]
            {
                new TypedValue((int) LispDataType.Nil),
                new TypedValue((int) LispDataType.DottedPair),
                new TypedValue((int) LispDataType.Text, ex.Message),
            });
    }
}

 The functions should always return a dotted pair. The first value is a boolean indicating success or not, the second value the result of the function if all works well or the error message if something goes wrong (not used in this case, but can be useful for debugging). Note that I added a _ before the function name to indicate that it should not be called directly.

 

Then in AutoLISP, write this kind of wrapper function:

 

(defun acos (value / result)
  (setq result (_acos value))
  (if (car result)
    (cdr result)
    (exit)
  )
)

If there is an error, you will get two errors displayed on the command line : the exception message written from your .NET code, and the standard error displayed by the exit function ( ; error: quit/exit). To hide the second one, you can write an error handler like this:

 

(defun my-error (msg)
  (if
    (/= (getvar "ERRNO") 0)
     (princ (strcat "\n; error: " msg))
  )
  (setq *error* *ini-err* *ini-err* nil)
  (princ)
)

 Set ERRNO to 0 in your lisp wrapper before calling exit or in your .NET exception handler to mute the message or (better) use custom error numbers (> 85).

Maxence DELANNOY
Manager
Add-ins development for Autodesk software products
http://wiip.fr
Message 5 of 10
_gile
in reply to: FRFR1426

Hi FRFR1426,

 

Thank you for your contribution, but I do not want any wrapper on the LISP side.

The goal would be creating an easily extensible LISP functions library using some helpers on the .NET side within a wrapper which catch any exception.

 

Using this wrapper and helpers, the upper acos function would be:

 

        [LispFunction("acos")]
        public double Acos(ResultBuffer resbuf)
        {
            return resbuf.CatchApply(rb =>
            {
                double d = rb.GetAsArray(1)[0].GetAsNumber();
                if (d < -1.0 || d > 1.0)
                    throw new LispException("the value must be between -1.0 and 1.0 inclusive");
                return Math.Acos(d);
            });
        }

 

 

For more information about this, you can see here.



Gilles Chanteau
Programmation AutoCAD LISP/.NET
GileCAD
GitHub

Message 6 of 10
FRFR1426
in reply to: _gile

Ok, I understand but as there is no clean integration between AutoLISP and .NET regarding error handling, you need to find a workaround. A modal alert box == no unit testing.

Maxence DELANNOY
Manager
Add-ins development for Autodesk software products
http://wiip.fr
Message 7 of 10
_gile
in reply to: FRFR1426

Salut Maxence,

 

Sans l'avatar, je ne t'avais pas reconnu.



Gilles Chanteau
Programmation AutoCAD LISP/.NET
GileCAD
GitHub

Message 8 of 10

Having to manually check a result returned by externally-defined LISP functions wouldn't be something I could recommend, as it could easily complicate the consuming LISP code. 

The problem is easily solved internally, by simply allowing an error string to be returned by externally-defined LISP functions, along with a result code signaling that an error occurred, and that the returned string should be passed to the (*error*) function, or be returned by a call to (vl-catch-all-error-message).  That would allow specific types of errors to be handled intelligently by consuming LISP code, if needed. If there is no error handler deined, AutoCAD would simply display the returned string in lieu of the generic message it currently displays (which is not terribly useful if you think about it).  

Message 9 of 10
FRFR1426
in reply to: _gile


_gile a écrit :

Salut Maxence,

 

Sans l'avatar, je ne t'avais pas reconnu.


Yes, I'm using our ADN account to post because we can now send our question to the ADN team if there is no answer given by the community. I have not found how to change the nickname, but I have updated my avatar. It makes the forum prettier Smiley Wink

Maxence DELANNOY
Manager
Add-ins development for Autodesk software products
http://wiip.fr
Message 10 of 10
StephenPreston
in reply to: FRFR1426

>>I have not found how to change the nickname

 

Click on 'Edit Account' at the top of the fiorum webpage.

Change your 'User Id'.

 

(I was wondering why you were posting using your ADN ID as your username :-).

Cheers,

Stephen Preston
Autodesk Developer Network

Can't find what you're looking for? Ask the community or share your knowledge.

Post to forums  

Autodesk DevCon in Munich May 28-29th


Autodesk Design & Make Report

”Boost