Kean Walmsley


  • About the Author
    Kean on Google+

April 2014

Sun Mon Tue Wed Thu Fri Sat
    1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30      







« Debugging using Express Editions | Main | Free API webcasts »

July 10, 2006

Calling ObjectARX functions from a .NET application

One of the really compelling features of .NET is its ability to call "legacy" unmanaged C++ APIs. I say "legacy", but we use this facility regularly to call APIs that are far from being considered defunct (the C++ version of ObjectARX is alive and kicking, believe me! :-).

Autodesk understands that our development partners have invested many years in application development, and can't afford to throw that investment away to support the latest & greatest (and sometimes "flavor of the month") programming technology. For example, over the years we've made sure it was possible to create a VB or VBA user-interface for an existing LISP application or now a .NET user-interface for an ObjectARX application. Sometimes we expose our own interoperability functions to help with this (such as LISP functions to call ActiveX DLLs), and in other cases we advise people on how best to leverage standard Microsoft platform technologies.

So... how do you call an ObjectARX function from VB.NET? The answer is Platform Invoke (or P/Invoke for short). Microsoft has not exposed the full functionality of the Win32 API through the .NET Framework - just as Autodesk has not exposed all of ObjectARX through AutoCAD's Managed API - but P/Invoke helps you get around this.

First, some background on what ObjectARX really is, and how P/Invoke can help us.

ObjectARX is a set of APIs that are exported from DLLs or EXEs. Most exported functions get "decorated" or "mangled" during compilation, unless there is a specific compiler directive not to (this is the case for all the old ADS functions, for instance - they are declared as extern "C" and are therefore not mangled). The compiler assigns a unique name based on the function signature, which makes sense: it is quite legal in C++ to have two functions with the same name, but not with identical arguments and return values. The decorated name includes the full function name inside it, which is why the below technique for finding the correct export works.

[ Note: this technique works well for C-style functions, or C++ static functions. It will not work on instance members (methods of classes), as it is not possible to instantiate an unmanaged object of the class that defines the class from managed code. If you need to expose a class method to managed code, you will need to write & expose some native C++ code that instantiates the class, calls the method and returns the result. ]

To demonstrate the procedure we're going to work through the steps needed to call acedGetUserFavoritesDir() from C# and VB.NET. This function is declared in the ObjectARX headers as:

extern Adesk::Boolean acedGetUserFavoritesDir( ACHAR* szFavoritesDir );

According to the ObjectARX Reference, "this function provides access to the Windows Favorites directory of the current user."


Step 1 - Identify the location of the export.

Fenton Webb, from DevTech EMEA, provided this handy batch file he uses for just this purpose:

[ Copy and paste this into a file named "findapi.bat", which you then place this into your AutoCAD application folder. You will need to run findapi from a command prompt which knows where to find dumpbin.exe - the Visual Studio Command Prompts created on installing VS will help you with this. ]

@echo off
if "%1" == "" goto usage
:normal
for %%i IN (*.exe *.dll *.arx *.dbx *.ocx *.ddf) DO dumpbin /exports %%i | findstr "%%i %1"
goto end
:usage
echo findapi "function name"
:end

You can redirect the output into a text file, of course, for example:

C:\Program Files\AutoCAD 2007>findapi acedGetUserFavoritesDir > results.txt

It'll take some time to work, as this batch file chunks through all the DLLs, EXEs, etc. in the AutoCAD application folder to find the results (it doesn't stop when it finds one, either - this enhancement is left as an exercise for the reader ;-).

Opening the text file will allow you to see where the acedGetUserFavoritesDir() function is exported:

[ from the results for AutoCAD 2007 ]

Dump of file acad.exe
        436  1B0 004B4DC0 ?acedGetUserFavoritesDir@@YAHPA_W@Z

A word of warning: the decorated names for functions accepting/returning strings changed between AutoCAD 2006 and 2007, because we are now using Unicode for string definition. Here is the previous declaration for 2004/2005/2006 (which was probably valid for as long as the function was defined, back in AutoCAD 2000i, if I recall correctly):

[ from the results for AutoCAD 2006 ]

Dump of file acad.exe
        357  161 00335140 ?acedGetUserFavoritesDir@@YAHPAD@Z

This is simply because the function signature has changed from taking a char* to an ACHAR* (a datatype which now resolves to a "wide" or Unicode string in AutoCAD 2007). A change in the function signature results in a change in the decorated name. This is straightforward enough, but it is worth bearing in mind the potential migration issue - a heavy dependency on decorated function names can lead to substantial migration effort if widespread signature changes are made in a release (as with AutoCAD 2007's support of Unicode).

Another warning: you will find a number of other functions exported from the various DLLs/EXEs that do not have corresponding declarations in the ObjectARX headers. These functions - while exposed - are not supported. Which means that you may be able to work out how they can be called, but use them at your own risk (which can be substantial). Unsupported APIs are liable to change (or even disappear) without notice.

Now we've identified where and how the function is exposed, we can create a declaration of this function we can use in our code.


Step 2 - Declare the function correctly in your code.

This is going to be slightly different depending on the programming language you're using.

VB developers will be used to using "Declare" to set-up P/Invoke from their projects. This ends up being translated by the compiler into calls to DllImport, which is also used directly in C#.

These declarations should be made at the class level (not within an individual function definition).

VB.NET

Private Declare Auto Function acedGetUserFavoritesDir Lib "acad.exe" Alias "?acedGetUserFavoritesDir@@YAHPA_W@Z" (<MarshalAs(UnmanagedType.LPWStr)> ByVal sDir As StringBuilder) As Boolean

C#

[DllImport("acad.exe", EntryPoint = "?acedGetUserFavoritesDir@@YAHPA_W@Z", CharSet = CharSet.Auto)]
public static extern bool acedGetUserFavoritesDir([MarshalAs(UnmanagedType.LPWStr)] StringBuilder sDir);

Notes:

  1. It's worth specifying the character set as "Auto" - which is not the default setting. The compiler does a good job of working out whether to use Unicode or ANSI, so it's easiest to trust it to take care of this.
  2. You will need to use the MarshalAs(UnmanagedType.LPWStr) declaration for Unicode string variables in 2007. This is true whether using Strings or StringBuilders.
  3. Use a StringBuilder for an output string parameter, as standard Strings are considered immutable. Strings are fine for input parameters.

Step 3 - Use the function in your code

[ I've omited the standard using/import statements, as well as the class & function declarations, to improve readability. ]

VB.NET

Dim ed As Editor = Application.DocumentManager.MdiActiveDocument.Editor
Dim sDir As New StringBuilder(256)
Dim bRet As Boolean = acedGetUserFavoritesDir(sDir)
If bRet And sDir.Length > 0 Then
        ed.WriteMessage("Your favorites folder is: " + sDir.ToString)
End If

C#

Editor ed = Application.DocumentManager.MdiActiveDocument.Editor;
StringBuilder sDir = new StringBuilder(256);
bool bRet = acedGetUserFavoritesDir(sDir);
if (bRet && sDir.Length > 0)
        ed.WriteMessage("Your favorites folder is: " + sDir.ToString());

Note: we declare the StringBuilder variable (sDir) as being 256 characters long. AutoCAD expects us to provide a sufficiently long buffer for the data to be copied into it.

On my system both code snippets resulted in the following being sent to AutoCAD's command-line:

Your favorites folder is: C:\My Documents\Favorites

So that's it: you should now be able to call global ObjectARX functions from .NET. This technique can also be used to call your own functions exposed from DLLs... which is one way to allow you to create fancy UIs with .NET and leverage existing C++ code (there are others, such as exposing your own Managed API).

For additional information on using P/Invoke, particularly with Win32, here is a really great resource.

TrackBack

TrackBack URL for this entry:
http://www.typepad.com/services/trackback/6a00d83452464869e200d8349e8afa53ef

Listed below are links to weblogs that reference Calling ObjectARX functions from a .NET application:

blog comments powered by Disqus

10 Random Posts