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.
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.
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).
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.
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.
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).
_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
>>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 :-).
Can't find what you're looking for? Ask the community or share your knowledge.