ObjectARX 2010: Dealing With Missing Exports

In the new ObjectARX 2010 SDK, Autodesk has added some new virtual member functions that are not exported as they should be. For example, the AcGiFaceData class has had two new virtual functions added for setting and getting the face transparency:

class AcGiFaceData: public AcRxObject
{

//[… deleted for brevity]
ACDB_PORT virtual AcDbObjectId* materials() const;
ACDB_PORT virtual AcGiMapper* mappers() const
virtual void setTransparency(const AcCmTransparency *transparency);
virtual
AcCmTransparency* transparency() const

private
:
AcGiImpFaceData *mpAcGiImpFaceData;
};

As you can see, whoever added the new functions neglected to prefix them with the ACDB_PORT macro. ACDB_PORT evaluates to __declspec(export), which tells the compiler to export the function. Since the macro is missing, the new functions are not exported from acdb18.dll.

Since these are virtual functions, you won’t have any problems calling them through a pointer to an AcGiFaceData object that was constructed by AutoCAD. The problem arises when you derive a class from AcGiFaceData. Since the functions are not exported, the linker has no way of resolving their address for creating the virtual function table of your derived class. This results in linker errors:

acrxEntryPoint.obj : error LNK2001: unresolved external symbol “public: virtual void __thiscall AcGiFaceData::setTransparency(class AcCmTransparency const *)” (?setTransparency@AcGiFaceData@@UAEXPBVAcCmTransparency@@@Z)
acrxEntryPoint.obj : error LNK2001: unresolved external symbol “public: virtual class AcCmTransparency * __thiscall AcGiFaceData::transparency(void)const ” (?transparency@AcGiFaceData@@UBEPAVAcCmTransparency@@XZ)

Following is an example that results in these errors:

class AcGiFaceDataEx: public AcGiFaceData
{

public
:
AcGiFaceDataEx() {}
~
AcGiFaceDataEx() {}
}
Test;

The only solution is to provide an implementation of the missing functions. In this case, it could be accomplished by something like this:

class AcGiFaceDataEx: public AcGiFaceData
{

AcCmTransparency* mpTransparency;
public
:
AcGiFaceDataEx() : mpTransparency( NULL ) {}
~
AcGiFaceDataEx() { delete mpTransparency; }
virtual
void setTransparency(const AcCmTransparency *transparency)
{

delete
mpTransparency;
mpTransparency = (transparency? new AcCmTransparency( *transparency ) : NULL);
}

virtual
AcCmTransparency* transparency() const { return mpTransparency; }
}
Test;

This will fix the linker errors, but there is no guarantee that it will work as intended. AutoCAD might access its internal transparency value directly without calling through the member functions, which means it would never “see” the transparency set through the replacement member functions. Furthermore, the addition of the new pointer member changes the size of the class, which causes AcGiFaceDataEx arrays to have a different memory footprint than AcGiFaceData arrays. Lastly, what if Autodesk fixes the problem in a future AutoCAD service pack?

The ideal solution should not change the size of the class. It should check at runtime whether the function is exported, then use the exported function if it exists. That way, code that is written now will use the exported function if and when it becomes available in a future version of AutoCAD. When the function is not exported, an alternate implementation must be provided. This is not an unusual scenario, and the solution I present for the specific case of AcGiFaceData can be adapted to the more general problem.

In the AcGiFaceData case, the missing functions are virtual functions. Knowing this, it is possible to use a trick to get the address of the real function. In the code below, the function getAcGiFaceData_vtable() constructs a temporary AcGiFaceData object, from which it extracts a pointer to the object’s virtual function table. The virtual function table is just an array of function pointers, so the address of the desired function can be obtained by indexing into the virtual function table. The question is, how far? By counting virtual functions and data members starting from the top of the class hierarchy: in this case, 6 virtual functions in AcRxObject plus 16 virtual functions in AcGiFaceData = 22.

Note that obtaining a function pointer this way relies on Visual C++ implementation details, but this is safe to do since all ObjectARX modules must be compiled in Visual C++.

Following is my solution to the missing AcGiFaceData functions:

#pragma warning(push)
#pragma warning(disable: 4608)
template < typename Src, typename Dest >
Dest force_cast( Src src )
{

union
_convertor { Dest d; Src s; _convertor() : d(0), s(0) {} } convertor;
convertor.s = src;
return
convertor.d;
}

#pragma warning(pop)

static
FARPROC* getAcGiFaceData_vtable()
{

static
FARPROC* rfVTable = *(FARPROC**)&AcGiFaceData();
return
(rfVTable? rfVTable : NULL);
}

void AcGiFaceData::setTransparency( const AcCmTransparency* transparency )
{

typedef
void (AcGiFaceData::*F_setTransparency)( const AcCmTransparency* );
static
F_setTransparency pfSetTransparency = force_cast< FARPROC, F_setTransparency >(GetProcAddress( GetModuleHandleA( “acdb18.dll”), “?setTransparency@AcGiFaceData@@UEAAXPEBVAcCmTransparency@@@Z” ));
if
( !pfSetTransparency )
{

static
FARPROC* rfVTable = getAcGiFaceData_vtable();
if
( rfVTable )
pfSetTransparency = force_cast< FARPROC, F_setTransparency >( rfVTable[22] );
}

if
( pfSetTransparency )
(
this->*pfSetTransparency)( transparency );
}

AcCmTransparency* AcGiFaceData::transparency() const
{

typedef
AcCmTransparency* (AcGiFaceData::*F_transparency)() const;
static
F_transparency pfTransparency = force_cast< FARPROC, F_transparency >(GetProcAddress( GetModuleHandleA( “acdb18.dll”), “?transparency@AcGiFaceData@@UEBAPEAVAcCmTransparency@@XZ” ));
if
( !pfTransparency )
{

static
FARPROC* rfVTable = getAcGiFaceData_vtable();
if
( rfVTable )
pfTransparency = force_cast< FARPROC, F_transparency >( rfVTable[23] );
}

if
( pfTransparency )
return
(this->*pfTransparency)();
return
NULL;
}

Missing Menu Madness

One of my many complaints about about the CUI system introduced in AutoCAD 2006 is that it’s not very friendly to third party developers. In my opinion, it’s not very friendly to end users either, but I digress…

One example of the unfriendly CUI is the case where a third party application installs a partial menu. In the pre-CUI days, adding a partial menu was an easy way to add an application specific menu to AutoCAD without making any changes to the end user’s existing menu files. If the application was later uninstalled, the uninstall script could remove its menu and clean up the registry, leaving no trace behind. CUI breaks that scenario.

When a partial menu is loaded into AutoCAD 2006 and later, the CUI system actually writes a reference to the new partial menu into the main .cui file. To make matters worse, there is no clean way to programmatically remove those references because there is no deterministic way to locate all the .cui files used by a specific instance of AutoCAD from an uninstall script running outside of AutoCAD.

This would not be the end of the world if AutoCAD simply ignored references to missing files, but it doesn’t. When the CUI command starts, it detects the missing files and displays an obnoxious message box that must be dismissed before the command will continue.
Unable to open file 'c:\program \files\manusoft\superpurge\spurge2004.cui'

Then to top it off, AutoCAD displays a second message box that also must be dismissed.
Unable to locate 'spurge2004.cui'. This file is defined in the main CUI file, but has been moved, renamed, or deleted from the following location: c:\program files\manusoft\superpurge\spurge2004.cui To reload this file, type cuiload on the command line.

I think it should be obvious even to an untrained UI designer how ridiculous this is.

In AutoCAD 2006 and 2007, the solution is to dutifully dismiss both dialogs, then when the CUI dialog finally appears, click [Apply] to rewrite the .cui file without the missing partial menu references. In AutoCAD 2008 and 2009, you have to first expand the ‘Partial CUI Files’ node in the Customization tab, then right-click on the unresolved partial menu file and select ‘Unload CUI File’, then click [Apply].

Debugging ObjectARX: Break on Exception

I have presented a class entitled High Octane ObjectARX at Autodesk University the past two years. In 2006 the focus was on project organization, and last year I focused more on techniques for supporting multiple versions of AutoCAD with a single Visual Studio solution, touching briefly on testing and profiling. For 2008 I plan to focus on debugging.

To get you in the mood, I decided to blog about a capability of Visual Studio that often goes unnoticed and unused: breaking on an exception. This works just like breaking on a code breakpoint, except the debugger breaks execution *before* the exception handler gets control, thus allowing you to get a clearer picture of what was happening immediately before the exception occurred.

In an ObjectARX project, all kinds of exceptions can occur. Sometimes they are perfectly normal exceptions caused by and handled by AutoCAD. Many times, especially during development, your code triggers an exception that causes AutoCAD to crash, and it’s not obvious what caused things to go haywire.

When unexpected things happen while running your code under the debugger, the first order of business is to inspect the debug output window to determine what happened. The debug output window (Debug->Windows->Output) displays a message whenever an exception occurs. Sometimes a series of exceptions occur, usually all caused by the same root problem. Breaking when the first exception occurs will likely yield the most useful information about the cause.

Once you examine the debug output window to determine the type of exception that occurred, open the Exceptions dialog (Debug->Exceptions) and tell Visual Studio to break when the exception is thrown:

Visual Studio contains a prepopulated list of common exceptions to choose from, or you can add new ones if you need to break on an exception that is not already listed. Many times, breaking when the exception is thrown allows you to inspect the stack trace to determine which one of your functions is to blame — and in the vast majority of cases, it will be your code that is at fault!

Hotel Autodesk

Steve Johnson asks “where have all the developers gone?“, and Deelip Menezes wonders in a comment whether Autodesk’s annual release cycle for AutoCAD is part of the problem. I’ve always said that an annual release cycle is untenable. The annual release cycle is motivated by Autodesk’s desire to make the subscription program look attractive. Has it worked?

Based on what I’ve read and heard through the grapevine, it has definitely worked. More customers than ever are choosing the subscription model. In many cases, the annual subscription business model actually works well for software like AutoCAD, providing benefits for both Autodesk and their customers. But subscription is not for everyone.

In typical greedy big corporate fashion, Autodesk have overplayed their hand. Instead of concentrating on those customers for whom subscription makes sense and leaving the others to choose a different model, the “more is always better” marketing machine kicked in. Ergo, the annual release cycle carrot and the AutoCAD retirement program stick were invented. [Oh sorry, it’s called the Autodesk Loyalty Program.]

I am already seeing the beginnings of a movement of discontent among Autodesk customers, and I expect the annual release cycle to collapse under its own weight within another year or two. In the meantime, tremendous damage is being done. Customers, third party developers, authors, and consultants all suffer under the strain of the annual release cycle, but that’s only the tip of the iceberg.

To pull off annual releases, Autodesk have to be working on multiple versions of AutoCAD in parallel. While AutoCAD 2006 was being beta tested, AutoCAD 2007 and 2008 were already under development, and AutoCAD 2009 was already in the planning stages. When CUI was first introduced to the public, the new ribbon UI was likely already in the planning stages. Is it any wonder that the angry feedback about CUI was ignored?

When you have an annual release cycle with 3 or 4 future releases on parallel tracks, you can’t just stop and fix a fundamental design flaw. All you can do is increase your public relations budget. The harm done to third party developers is substantial, but the inability to shift gears and correct fundamental design flaws is the real travesty of the annual release cycle.

AU 2007 Post Mortem

The Hot List

  • Carl Bass munching on fast food in the food court, and spotted throughout the week yakking with average folks in the halls.
  • Staggered classes reduced the lunchtime crush experienced at AU 2006.
  • More ObjectARX programming classes this year. I’m pushing for creating a new “Developer” track for AU 2008.
  • The Matt Murphy Head (peace be upon him).
  • Meeting a lot of familiar faces and a few new ones!

The Cold List

  • The food was worse this year than even AU ’97 in Los Angeles. At least AU ’97 had a Burger King nearby for someone on a budget; here in Las Vegas I ended up spending several hundred dollars over the course of the week just for food.
  • Pete Kelsey’s band, Dr. Ruth, didn’t play. Rumor has it that the AU organizers refused to give them space. Dr. Ruth has become an AU staple, and it just wasn’t the same without them.
  • Shaan Hurley’s gut-wrenching regimen for clearing 20 year old meat byproducts from various body cavities.

I presented the first of two ObjectARX roundtable discussions this year (the second one, hosted by Stephen Preston, also covered .NET). This was an experimental format. I liked it, and I thought it went well. If you were there, I’d like to hear your suggestions for making it better next year. I will be following up with some more posts covering topics raised in this and other AU classes.