As promised, last week, today we’re going to look at a technique pointed out to me by Viru Aithal for calling unexposed ObjectARX member functions via P/Invoke.
It sometimes happens that ObjectARX (unmanaged C++) methods fail to get exposed immediately via .NET, which is currently the case for MLeader.Scale (both getting and setting this property). The technique shown in this post allows you to work around this, prior to the managed API in AutoCAD being updated to include it. It’s worth noting that if you need a more complex exposure via .NET – as Dan Smith sometimes reminds me (thanks, Dan :-) – it’s preferably to go directly with C++/CLI rather than P/Invoke. This approach isn’t for everyone, of course – you should be comfortable with C++ to consider it – but I did want to just mention that (pre-emptively ;-).
There are a few steps involved in defining your P/Invoke statements, as covered in this old post of mine and also here (a nice article which inspired me to include the System.Security.SuppressUnmanagedCodeSecurity attribute in our class).
I won’t go into that side of things, again, but will cut to the chase: when P/Invoking a method – which is really just a normal function with a “this” argument denoting the object for the method to work on – we simply need to pass in the UnmanagedObject property of our .NET object prior to the other arguments (making sure the method signature has been DllImported using the ThisCall calling convention).
Here, for instance, we get the value of our MLeader’s scale property:
double mls = Scale(ent.UnmanagedObject);
And here we set it to double the original value:
SetScale(ent.UnmanagedObject, mls * 2);
Here’s the full C# code defining our DMLS (DoubleMLeaderScale) command:
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using System.Runtime.InteropServices;
using System.Security;
using System;
namespace PInvokeMethods
{
public class Commands
{
[SuppressUnmanagedCodeSecurity]
[DllImport(
"acdb18.dll",
CallingConvention = CallingConvention.ThisCall,
EntryPoint = "?scale@AcDbMLeader@@QBENXZ"
)]
private static extern double Scale(IntPtr mleader);
[DllImport(
"acdb18.dll",
CallingConvention = CallingConvention.ThisCall,
EntryPoint =
"?setScale@AcDbMLeader@@QAE?AW4ErrorStatus@Acad@@N@Z"
)]
private static extern int SetScale(
IntPtr mleader, double scale
);
[CommandMethod("DMLS")]
public static void DoubleMLeaderScale()
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Editor ed = doc.Editor;
PromptEntityOptions peo =
new PromptEntityOptions("\nSelect MLeader");
peo.SetRejectMessage(
"\nMust be an MLeader."
);
peo.AddAllowedClass(typeof(MLeader), false);
PromptEntityResult per = ed.GetEntity(peo);
if (per.Status != PromptStatus.OK)
return;
Transaction tr =
doc.TransactionManager.StartTransaction();
using (tr)
{
try
{
// Open the selected entity
Entity ent =
(Entity)tr.GetObject(
per.ObjectId,
OpenMode.ForRead
);
// We use is instead of as, because we don't
// actually need an MLeader object reference
// to P/Invoke the unmanaged methods
if (ent is MLeader)
{
// Get the scale
double mls = Scale(ent.UnmanagedObject);
// Double it and write it back to the MLeader
ent.UpgradeOpen();
SetScale(ent.UnmanagedObject, mls * 2);
}
tr.Commit();
}
catch (Autodesk.AutoCAD.Runtime.Exception ex)
{
ed.WriteMessage("\nException: {0}", ex.Message);
}
}
}
}
}
The command works predictably enough. Here’s an MLeader prior to modification:
And here it is after having been updated by the DMLS command: