Using the P/Invoke Interop Assistant to help call ObjectARX from .NET

Thanks to Gopinath Taget, from DevTech Americas, for letting me know of this tool's existence.

I've often battled to create Platform Invoke signatures for unmanaged C(++) APIs in .NET applications, and it seems this tool is likely to save me some future pain. The tool was introduced in an edition of MSDN Magazine, earlier this year.

[For those unfamiliar with P/Invoke, I recommend this previous post.]

While the Interop Assistant can be used for creating function signatures to call into .NET assemblies from unmanaged applications, I'm usually more interested in the reverse: calling unmanaged functions from .NET.

Let's take a quick spin with the tool, working with some functions from the ObjectARX includes folder...

A great place to start is the aced.h file, as it contains quite a few C-standard functions that are callable via P/Invoke.

After installing and launching the tool, the third tab can be used for generation of C# (DllImport) or VB.NET (Declare) signatures for unmanaged function(s). To get started we'll go with an easy one:

void acedPostCommandPrompt();

After posting the code into the "Native Code Snippet" window, we can generate the results for C#:

acedPostCommandPrompt from C#

And for VB.NET:

acedPostCommandPrompt from VB.NET

Taking a quick look at the generated signature (I'll choose the C# one, as usual), we can see it's slightly incomplete:

public partial class NativeMethods

{

  /// Return Type: void

  [System.Runtime.InteropServices.DllImportAttribute(

    "<Unknown>",

    EntryPoint = "acedPostCommandPrompt")

  ]

  public static extern void acedPostCommandPrompt();

}

We need to replace "<Unknown>" with the name of the EXE or DLL from which this function is exported: this is information that was not made available to the tool, as we simply copy & pasted a function signature from a header file. This previous post (the one that gives an introduction to P/Invoke) shows a technique for identifying the appropriate module to use.

Now let's try an altogether more complicated header:

int acedCmdLookup(const ACHAR* cmdStr, int globalLookup,

                  AcEdCommandStruc* retStruc,

                  int skipUndef = FALSE);

To get started, we simply paste this into the "Native Code Snippet" window, once again:

acedCmdLookup - step 1

There are a few problems. Copying in the definition for ACHAR (from AdAChar.h), solves the first issue, but copying in the definition for AcEdCommandStruc (from accmd.h) identifies two further issues:

acedCmdLookup - step 2

The first is easy to fix - we just copy and paste the definition of AcRxFunctionPtr from accmd.h. The second issue - defining AcEdCommand - is much more complicated, and in this case there isn't a pressing need for us to bother as the information contained in the rest of the AcEdCommandStruc structure is sufficient for this example. So we can simple set this to a void* in the structure:

acedCmdLookup - step 3

OK, so we now have our valid P/Invoke signature, which we can integrate into a C# test application:

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.Runtime;

using System.Runtime.InteropServices;

namespace PInvokeTest

{

  public delegate void AcRxFunctionPtr();

  [StructLayoutAttribute(LayoutKind.Sequential)]

  public struct AcEdCommandStruc

  {

    public AcRxFunctionPtr fcnAddr;

    public int flags;

    public System.IntPtr app;

    public System.IntPtr hResHandle;

    public System.IntPtr cmd;

  }

  [StructLayoutAttribute(LayoutKind.Sequential)]

  public struct HINSTANCE__

  {

    public int unused;

  }

  public class Commands

  {

    [DllImportAttribute(

      "acad.exe",

      EntryPoint = "acedCmdLookup")

    ]

    public static extern int acedCmdLookup(

      [InAttribute()]

      [MarshalAsAttribute(UnmanagedType.LPWStr)] string cmdStr,

      int globalLookup,

      ref AcEdCommandStruc retStruc,

      int skipUndef

    );

    [CommandMethod("LC")]

    static public void LookupCommand()

    {

      Document doc =

        Application.DocumentManager.MdiActiveDocument;

      Database db = doc.Database;

      Editor ed = doc.Editor;

      PromptStringOptions pso =

        new PromptStringOptions(

          "\nEnter name of command to look-up: "

        );

      PromptResult pr = ed.GetString(pso);

      if (pr.Status != PromptStatus.OK)

        return;

      AcEdCommandStruc cs = new AcEdCommandStruc();

      int res =

        acedCmdLookup(pr.StringResult, 1, ref cs, 1);

      if (res == 0)

        ed.WriteMessage(

          "\nCould not find command definition." +

          " It may not be defined in ObjectARX."

        );

      else

      {

        CommandFlags cf = (CommandFlags)cs.flags;

        ed.WriteMessage(

          "\nFound the definition of {0} command. " +

          " It has command-flags value of \"{1}\".",

          pr.StringResult.ToUpper(),

          cf

        );

        PromptKeywordOptions pko =

          new PromptKeywordOptions(

            "\nAttempt to invoke this command?"

          );

        pko.AllowNone = true;

        pko.Keywords.Add("Yes");

        pko.Keywords.Add("No");

        pko.Keywords.Default = "No";

        PromptResult pkr =

          ed.GetKeywords(pko);

        if (pkr.Status == PromptStatus.OK)

        {

          if (pkr.StringResult == "Yes")

          {

            // Not a recommended way of invoking

            // commands. Use SendCommand() or

            // SendStringToExecute()

            cs.fcnAddr.Invoke();

          }

        }

      }

    }

  }

}

Our LC command looks up a command based on its command-name and tells us the flags that were used to define it (Modal, Session, Transparent, UsePickSet, etc.). It will then offer the option to invoke it, as we have the function address as another member of the AcEdCommandStruc populated by the acedCmdLookup() function. I do not recommend using this approach for calling commands - not only are we relying on a ill-advised mechanism in ObjectARX for doing so, we're calling through to it from managed code - we're just using this as a more complicated P/Invoke example and investigating what can (in theory) be achieved with the results. If you're interested in calling commands from .NET, see this post.

By the way, if you're trying to get hold of P/Invoke signature for the Win32 API, I recommend checking here first.

  1. J. Daniel Smith Avatar
    J. Daniel Smith

    Generally, I much prefer C++ Interop (previously known as IJW-It Just Works) over P/Invoke. Although for *simple* methods in an ARX, P/Invoke can be handy as you can't easily add a .NET reference to an ARX (it has to be a .DLL).

    Frankly, if you need a tool to figure out the P/Invoke signature, you should probalby be using C++ Interop and creating a more .NET-friendly API.

  2. I have to disagree with Dan's comment.

    I have a project underway at the moment, that is 99.9% managed code, with the balance being in a C++/CLI based mixed mode DLL.

    Unfortunately, that small amount of native code means I have to create seperate builds of the mixed-mode DLL for each supported platform (e.g., 32 and 64 bit), as well as for 'big R' AutoCAD upgrades that break bindary compatibility.

    In contrast and theoretically at least, pure managed code should be portable.

    I would much prefer to have 100% managed code with P/Invoke, as opposed to native DLLs that require multiple builds.

  3. When I started playing around with Mono I found another reason to use P/Invoke: Mono doesn't support mixed-mode assemblies. So there's another example of why pure managed C++/CLI libraries are sometimes necessary.

    When you want a non-mixed assembly implemented in C++/CLI, you can't use IJW (it uses unmanaged thunks) and you must use P/Invoke.

    Thanks for posting the note and the links, Kean.

  4. You should try p/invoker from http://www.pinvoker.com - it'll do all this automatically for you from a .h and a .dll file.

  5. Hi Kean,

    I’ve recently required the use an ARX only function for a C#.NET routine. As luck would have it an example of calling the necessary ARX functions was included with the BREP WebCast conducted by Autodesk. After incorporating the technique (and subsequent retesting the WebCast example) I encounter a time delayed error which crashes AutoCAD (2009) .

    This thread at “TheSwamp” provides additional background on the problem. (theswamp.org/ind...)

    <as mentioned="" above="">Is this an example of “. . . . it is not possible to instantiate an unmanaged object of the class that defines the class from managed code. . . ."?

    And/or

    Given that the ARX functions in question are listed as “For internal use only “, should their use be avoided?

    I’d certainly appreciate any time you could devote to this matter,

    Sean Tessier

  6. Kean Walmsley Avatar
    Kean Walmsley

    Hi Sean,

    I'll check in with Gopi, the presenter of this session.

    I can't say whether there's an issue caused by the P/Invoke call - I've not had to use this technique myself - but I do know I hit a crash when using the Brep API to traverse a 3D solid when I forgot to Dispose the brep itself (something that appears commented out in the code you posted to The Swamp).

    Regards,

    Kean

  7. Kean Walmsley Avatar
    Kean Walmsley

    Hi Sean,

    Gopi tells me that the P/Invoked function was holding onto an object reference. You need to add this line of code where you would normally dispose of bdy:

    LibWrap.CreateSurfBdy(bdy.UnmanagedObject, IntPtr.Zero);

    I hope this helps,

    Kean

  8. Sean Tessier Avatar

    Thanks Kean,

    Yes, that does fix things. As predicted, I’m very much grateful for this particular bit of support, as well as the general guidance provided by this blog.

    Enjoy your weekend,

    Sean

Leave a Reply

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