Author Topic: P/Invoke acedSSNameX safely  (Read 4107 times)

0 Members and 1 Guest are viewing this topic.

kaefer

  • Guest
P/Invoke acedSSNameX safely
« on: April 24, 2012, 02:51:09 PM »
There's a contribution from Philippe Leefsma on ADN DevBlog - AutoCAD, titled Retrieving nested entities under cursor aperture using .Net API. That got me thinking: 1) why should code handling a pointer to a pointer be marked unsafe, and 2) why would the consumers of an unmanaged data structure need to define the structure, if it's already exposed by the API?

This approach led me to dump definitions of ads_name and resbuf, replace them with variables of type AdsName and IntPtr, and to change the extern declaration of acedSSNameX from
Code: [Select]
    public unsafe static extern PromptStatus acedSSNameX(
        resbuf** rbpp, ref ads_name ss, int i);
to
Code: [Select]
    public static extern PromptStatus acedSSNameX(
        out IntPtr rbpp, ref AdsName ss, int i)

Of course, not all is well, since AdsName was a pair of int32 until recently (R 18.1?), so that may fail with previous versions. Am I at least correct in my handling of the ResultBuffer pointer?

Code - C#: [Select]
  1. // Originally by AutoCAD devblogger Philippe Leefsma,
  2. // http://adndevblog.typepad.com/autocad/2012/04/retrieving-nested-entities-under-cursor-aperture-using-net-api.html
  3.  
  4. class ArxImports
  5. {
  6.     [DllImport("acad.exe",
  7.         CallingConvention = CallingConvention.Cdecl,
  8.         CharSet = CharSet.Unicode,
  9.         ExactSpelling = true)]
  10.     public static extern PromptStatus acedSSGet(
  11.         string str, IntPtr pt1, IntPtr pt2,
  12.         IntPtr filter, out AdsName ss);
  13.  
  14.     [DllImport("acad.exe",
  15.         CallingConvention = CallingConvention.Cdecl,
  16.         CharSet = CharSet.Unicode,
  17.         ExactSpelling = true)]
  18.     public static extern PromptStatus acedSSFree(ref AdsName ss);
  19.  
  20.     [DllImport("acad.exe",
  21.         CallingConvention = CallingConvention.Cdecl,
  22.         CharSet = CharSet.Unicode,
  23.         ExactSpelling = true)]
  24.     public static extern PromptStatus acedSSLength(
  25.         ref AdsName ss, out int len);
  26.  
  27.     [DllImport("acad.exe",
  28.         CallingConvention = CallingConvention.Cdecl,
  29.         CharSet = CharSet.Unicode,
  30.         ExactSpelling = true)]
  31.     public static extern PromptStatus acedSSNameX(
  32.         out IntPtr rbpp, ref AdsName ss, int i);
  33. }
  34.  
  35. public class CursorDetectCls
  36. {
  37.     Editor _ed;
  38.     bool pointMonitorActive;
  39.  
  40.     public CursorDetectCls()
  41.     {
  42.         _ed = acadApp.DocumentManager.MdiActiveDocument.Editor;
  43.         pointMonitorActive = false;
  44.     }
  45.  
  46.     [CommandMethod("UnderCursorNestedStart")]
  47.     public void UnderCursorNestedStart()
  48.     {
  49.         if (!pointMonitorActive)
  50.         {
  51.             //Set up PointMonitor event
  52.             _ed.PointMonitor +=
  53.                 new PointMonitorEventHandler(PointMonitorMulti);
  54.             pointMonitorActive = true;
  55.         }
  56.     }
  57.     [CommandMethod("UnderCursorNestedStop")]
  58.     public void UnderCursorNestedStop()
  59.     {
  60.         if (pointMonitorActive)
  61.         {
  62.             //Remove PointMonitor event
  63.             _ed.PointMonitor -=
  64.                 new PointMonitorEventHandler(PointMonitorMulti);
  65.            pointMonitorActive = false;
  66.         }
  67.     }
  68.     void PointMonitorMulti(object sender, PointMonitorEventArgs e)
  69.     {
  70.         //Filters only block references (INSERT)
  71.         //that are on layer "0"
  72.         ResultBuffer resbuf = new ResultBuffer(
  73.             new TypedValue(-4, "<and"),
  74.             new TypedValue(0, "INSERT"),
  75.             new TypedValue(8, "0"),
  76.             new TypedValue(-4, "and>"));
  77.  
  78.         ObjectId[] ids = FindAtPointNested(
  79.             e.Context.RawPoint,
  80.             true,
  81.             resbuf.UnmanagedObject);
  82.  
  83.         //Dump result to commandline
  84.         foreach (ObjectId id in ids)
  85.         {
  86.             _ed.WriteMessage("\n - Entity: {0} [Id:{1}] ",
  87.                 id.ObjectClass.Name,
  88.                 id.ToString());
  89.         }
  90.     }
  91.  
  92.     //Retruns ObjectIds of entities at a specific position
  93.     //Including nested entities in block references
  94.     ObjectId[] FindAtPointNested(
  95.         Point3d worldPoint,
  96.         bool selectAll,
  97.         IntPtr filter)
  98.     {
  99.         System.Collections.Generic.List<ObjectId> ids =
  100.             new System.Collections.Generic.List<ObjectId>();
  101.  
  102.         Matrix3d wcs2ucs =
  103.             _ed.CurrentUserCoordinateSystem.Inverse();
  104.  
  105.         Point3d ucsPoint = worldPoint.TransformBy(wcs2ucs);
  106.  
  107.         string arg = selectAll ? "_:E:N" : "_:N";
  108.  
  109.         IntPtr ptrPoint = Marshal.UnsafeAddrOfPinnedArrayElement(
  110.             worldPoint.ToArray(), 0);
  111.  
  112.         AdsName sset;
  113.  
  114.         PromptStatus prGetResult = ArxImports.acedSSGet(
  115.             arg, ptrPoint, IntPtr.Zero, filter, out sset);
  116.  
  117.         int len;
  118.         ArxImports.acedSSLength(ref sset, out len);
  119.  
  120.         IntPtr pRb;
  121.         for (int i = 0; i < len; ++i)
  122.             {
  123.                 if (ArxImports.acedSSNameX(out pRb, ref sset, i) !=
  124.                     PromptStatus.OK)
  125.                     continue;
  126.  
  127.                 //Create a managed ResultBuffer from the unmanaged Resultbuffer pointer
  128.                 using (ResultBuffer rbMng = (ResultBuffer)DisposableWrapper.Create(
  129.                     typeof(ResultBuffer),
  130.                     pRb,
  131.                     true))
  132.                 {
  133.                     foreach (TypedValue tpVal in rbMng)
  134.                     {
  135.                         //Only interested if it's an ObjectId
  136.                         if (tpVal.TypeCode == 5006) //RTENAME
  137.                             ids.Add((ObjectId)tpVal.Value);
  138.                     }
  139.                 }
  140.             }
  141.  
  142.         ArxImports.acedSSFree(ref sset);
  143.  
  144.         return ids.ToArray();
  145.     }
  146. }

SGP2012

  • Guest
Re: P/Invoke acedSSNameX safely
« Reply #1 on: April 24, 2012, 10:16:38 PM »
Hi Kaefer,

If you're improving on code posted to DevBlog, it would be great if you could see your way to providing a hyperlink as a comment on the DevBlog post.

Cheers,

Stephen

TheMaster

  • Guest
Re: P/Invoke acedSSNameX safely
« Reply #2 on: April 25, 2012, 06:30:37 PM »
Quote
Of course, not all is well, since AdsName was a pair of int32 until recently (R 18.1?), so that may fail with previous versions. Am I at least correct in my handling of the ResultBuffer pointer?

Yes, using 'out IntPtr' is equivalent to resbuf**, and you can use 'int/long[2]'
for the adsname argument, instead of out AdsName. If the AdsName struct
changed from int to long you would need to have release-dependent code.

I don't know what the author of the article was thinking, but there's no
need for any unsafe code for what that code is doing.

If one really wanted to do what that article shows (which I would not
advise, because what the article doesn't point out is that doing that
will overwrite the user's Previous selection set), you only need to
P/Invoke acedSSGet(), and the rest can be done entirely in managed
code.

See:

  Autodesk.AutoCAD.ApplicationServices.Marshaler.AdsNameToSelectionSet()

To get the argument (a pointer to a 2-element int[] or long[] array), you would
use something like this:

Code - C#: [Select]
  1.  
  2. long[2] array = // assigned to result of acedSSGet():
  3.  
  4. SelectionSet ss = null;
  5. GCHandle handle = GCHandle.Alloc(array, GCHandleType.Pinned);
  6. try
  7. {
  8.      ss = Marshaler.AdsNameToSelectionSet( handle.AddrOfPinnedObject() );
  9. }
  10. finally
  11. {
  12.     handle.Free();
  13. }
  14.  
  15.  

Once you have the managed selection set, the SelectedObject class
exposed by the SelectionSet provides access to nested entities and
does all the grunt work like calling acedSSNameX() and so forth.

edit:

After checking what I wrote above, it turns out that the AdsName struct
is the best way to go, because it allows you to entirely avoid release-
dependent code.

Use an AdsName as the last argument to acedSSGet() (with the 'ref'
modifier, because it's value will be written and used in the managed
caller), and use something like this to convert the AdsName result
to a managed selection set:

Code - C#: [Select]
  1.  
  2.   AdsName adsname    // assigned to result of acesSSGet():
  3.   SelectionSet ss = null;
  4.   using( PinnedObject pinned = new PinnedObject( adsname ) )
  5.   {
  6.        ss = Marshaler.AdsNameToSelectionSet( pinned );
  7.   }
  8.  
  9.   // Wrapper for GCHandle to correctly handle Free():
  10.  
  11.   public class PinnedObject : IDisposable
  12.   {
  13.     private GCHandle handle;
  14.  
  15.     public PinnedObject( object obj )
  16.     {
  17.       if( obj == null )
  18.         throw new ArgumentNullException("objectToPin");
  19.       this.handle = GCHandle.Alloc( obj, GCHandleType.Pinned );
  20.     }
  21.  
  22.     public IntPtr Address
  23.     {
  24.       get
  25.       {
  26.         return handle.AddrOfPinnedObject();
  27.       }
  28.     }
  29.  
  30.     public static implicit operator IntPtr( PinnedObject src )
  31.     {
  32.       return src.handle.AddrOfPinnedObject();
  33.     }
  34.  
  35.     void IDisposable.Dispose()
  36.     {
  37.       if( handle.IsAllocated )
  38.       {
  39.         handle.Free();
  40.       }
  41.     }
  42.   }
  43.  
  44.  
  45.  

The above should work regardless of the release or what
the AdsName struct is defined to be.

One caveat I will point out, which speaks directly to what is
(IMO) a major design flaw of the managed API, is that doing
this with a very large selection set will not be terribly fast,
because (the major design flaw), managed selection sets are
not wrappers around native selection sets (yes, IMO they
should be wrappers, since this issue seems to keep cropping
up and in fact, AdsNameToSelectionSet() and the AdsName
struct are the 'band-aids').

« Last Edit: April 26, 2012, 05:07:51 PM by TheMaster »

TheMaster

  • Guest
Re: P/Invoke acedSSNameX safely
« Reply #3 on: April 26, 2012, 08:41:54 AM »
Hi Kaefer,

If you're improving on code posted to DevBlog, it would be great if you could see your way to providing a hyperlink as a comment on the DevBlog post.

Cheers,

Stephen

I would just remove that article because it jumps through hoops needlessly, and doesn't make the reader aware of the fact that using that code interferes with the Previous selection. Doing nothing more than moving the pickbox over one or more entities causes the previous selection to be changed.

That's no good


TheMaster

  • Guest
Re: P/Invoke acedSSNameX safely
« Reply #4 on: April 26, 2012, 02:18:20 PM »
Code - C#: [Select]
  1. // Originally by AutoCAD devblogger Philippe Leefsma,
  2. // http://adndevblog.typepad.com/autocad/2012/04/retrieving-nested-entities-under-cursor-aperture-using-net-api.html
  3.  
  4. class ArxImports
  5. {
  6.     [DllImport("acad.exe",
  7.         CallingConvention = CallingConvention.Cdecl,
  8.         CharSet = CharSet.Unicode,
  9.         ExactSpelling = true)]
  10.     public static extern PromptStatus acedSSGet(
  11.         string str, IntPtr pt1, IntPtr pt2,
  12.         IntPtr filter, out AdsName ss);
  13.  
  14.    ....
  15. }
  16.  

The acedSSGet() declaration and call are also not nearly as simple
as it could be.

Declaring the first point argument as 'out Point3d' should work.
If for some reason that doesn't work, you can declare the first
point argument as 'out double[]', and just assign the result of
ToArray() to a double[] and pass that.


kaefer

  • Guest
Re: P/Invoke acedSSNameX safely
« Reply #5 on: April 29, 2012, 05:13:29 AM »
, you only need to
P/Invoke acedSSGet(), and the rest can be done entirely in managed
code.

I'm afraid that this apparent alternative doesn't work as one might expect. It seems the nested objects are ignored when the AdsName is converted into a managed SelectionSet. I take the GetSubentities() method as the way to extract FullSubentityPaths, but it comes up empty.

, managed selection sets are
not wrappers around native selection sets (yes, IMO they
should be wrappers, since this issue seems to keep cropping
up and in fact, AdsNameToSelectionSet() and the AdsName
struct are the 'band-aids').

As only a subset of the available selection modes of acedSSGet() are implemented as managed methods, it would be interesting to know the design rationale for the omissions. The pain of P/Invoking the unmanaged functions for selection set handling isn't that great as to make it infeasible, as the current example demonstrates, despite its flaws.

The acedSSGet() declaration and call are also not nearly as simple
as it could be.

Declaring the first point argument as 'out Point3d' should work.
If for some reason that doesn't work, you can declare the first
point argument as 'out double[]', and just assign the result of
ToArray() to a double[] and pass that.

I've found that this is what has to be done when translating the code to F#. Not only that you can't null a ValueType, the compiler enforces the not-null-constraint for byref<ValueType> too. That's why an array is the better choice to convey the optionality of the Point3d arguments.

Code - F#: [Select]
  1. module ArxImports =
  2.     [<DllImport("acad.exe",
  3.         CallingConvention = CallingConvention.Cdecl,
  4.         CharSet = CharSet.Unicode,
  5.         ExactSpelling = true)>]
  6.     let acedSSGet
  7.         (   str : string , pt1 : float[], pt2 : float[],
  8.             filter : nativeint, [<Out>] ss : byref<AdsName> ) =
  9.                 PromptStatus.OK

The OutAttribute isn't strictly necessary; all variables passed as byref<'T> (ref in C#) are required to be mutable. To avoid the capture of mutables by closures (which isn't allowed and severely restricts their usefulness in this scenario), I use heap-allocated reference cells instead (declared with F# ref keyword, no equivalent in C#).

Code - F#: [Select]
  1. type AcedSSGet(str, pt1, pt2, filter)  =
  2.     let sset = ref(AdsName())
  3.     let len = ref 0
  4.     let pRb = ref System.IntPtr.Zero
  5.     let res = ArxImports.acedSSGet(str, pt1, pt2, filter, &sset.contents)
  6.     let sso = if res = PromptStatus.OK then Some sset else None
  7.  
  8.     member __.SSmap = seq{
  9.         match sso with
  10.         | None -> ()
  11.         | Some sset->
  12.             let res = ArxImports.acedSSLength(&sset.contents, &len.contents)
  13.             if res = PromptStatus.OK then
  14.                 for i in 0 .. !len - 1 do
  15.                     let res = ArxImports.acedSSNameX(&pRb.contents, &sset.contents, i)
  16.                     if res = PromptStatus.OK then
  17.                         yield Marshaler.ResbufToTypedValues !pRb }
  18.  
  19.     interface System.IDisposable with
  20.         member __.Dispose() =
  21.             match sso with
  22.             | None -> ()
  23.             | Some sset-> ArxImports.acedSSFree &sset.contents |> ignore

The signature of the constructor is new : str:string * pt1:float [] * pt2:float [] * filter:nativeint -> AcedSSGet, it could be wrapped with a static method or another class for which all arguments except str are optional.

Code - F#: [Select]
  1. type AcedSSGetOptionalArgs(str, ?pt1 : Point3d, ?pt2 : Point3d, ?filter : SelectionFilter) =
  2.     let ptoToArray =
  3.         Option.map (fun (p : Point3d) ->
  4.             p.ToArray() )
  5.  
  6.     let sfoToPtr =
  7.         Option.map (fun (sf : SelectionFilter) ->
  8.             Marshaler.TypedValuesToResbuf(sf.GetFilter()) )
  9.  
  10.     member __.Value =
  11.         use sset =
  12.             new AcedSSGet(
  13.                 str,
  14.                 defaultArg (ptoToArray pt1) null,
  15.                 defaultArg (ptoToArray pt2) null,
  16.                 defaultArg (sfoToPtr filter) System.IntPtr.Zero )
  17.         sset.SSmap |> Seq.toArray

TypedValue arrays as return values aren't particularly useful here, it was just to show the umanaged data of what could have been a managed SelectedObject.

TheMaster

  • Guest
Re: P/Invoke acedSSNameX safely
« Reply #6 on: April 30, 2012, 07:24:34 AM »
First, I don't know what point there is to all of this, considering that any use of acedSSGet() is going to trash the user's Previous selection set, which is something that should never be done from a PointMonitor event handler, if at all.

In any event, I believe converting the native selection set to a managed selection set does work, but only for a single picked object, where the item in the selection set is a PickPointSelectedObject. If you use the managed equivalent of the :N option with GetSelection(), you only get subentities that are selected using a pick point. Objects selected by window/crossing, etc. will not include nested entities.  The :E option (select everything in aperture) is actually more like a window/crossing selection.

Getting multiple nested entities in the pickbox/aperture is something that one would rarely if ever need to do.

For the more common objective of getting a single nested object at the cursor and its containers, this is the wrong way to go about it. It's relatively trivial to use the Editor's GetNestedEntity() method with the NonInteractivePickPoint set in the PromptNestedEntityOptions. That also has the benefit of not trashing the user's Previous selection set.
« Last Edit: April 30, 2012, 07:37:43 AM by TheMaster »