• entries
    58
  • comments
    218
  • views
    114352

Interop Death

Sign in to follow this  

733 views

Lots of people look at SlimDX, hear its premise, and then say "Oh, that sounds simple. How hard could it be to wrap?" While the general case is indeed quite easy to handle, you quickly run into problems with edge cases and strange scenarios that throw your well-laid base code out of whack.

For a good example of this, let's walk through the implementation of a more complex interface. D3DX exposes a type called ID3DXAnimationSet, which is used by ID3DXAnimationController to define and control animations. ID3DXAnimationSet is a base class; two other classes derive from it to provide implementations: ID3DXCompressedAnimationSet and ID3DXKeyframedAnimationSet. In addition, advanced users can derive from ID3DXAnimationSet to provide their own implementations. So how do we go about making this work for both .NET and COM?

To start with, we create the .NET class that will represent ID3DXAnimationSet. Since we keep track of all COM objects in an object table to ensure that each object is represented by exactly on .NET reference, we need to derive said class from our base object which is conveniently called ComObject. In addition, we need to ensure that all object classes implement a certain set of properties and methods, but we can't use templates because they don't mesh well with .NET. Therefore, we have two different macros, COMOBJECT() and COMOBJECT_BASE() that provide the implementations we need a mostly generic manner.

Let's see what we have so far:

public ref class AnimationSet abstract : public ComObject
{
COMOBJECT_BASE(ID3DXAnimationSet)
};




Alright, so far so good. We have an abstract AnimationSet class that acts as a proper ComObject and represents ID3DXAnimationSet. Next, we fill in the methods that are exposed by ID3DXAnimationSet, and make them act as a simple wrapper over the internal pointer. We also define them to be virtual, so that future classes can override them.


public ref class AnimationSet abstract : public ComObject
{
COMOBJECT_BASE(ID3DXAnimationSet);

public:
virtual int GetAnimationIndex( System::String^ name );
virtual System::String^ GetAnimationName( int index );
virtual System::IntPtr GetCallback( double position, CallbackSearchFlags flags, [Out] double% callbackPosition );
virtual AnimationOutput GetTransformation( double periodicPosition, int animation );
virtual double GetPeriodicPosition( double position );

virtual property System::String^ Name { System::String^ get(); }
virtual property int AnimationCount { int get(); }
virtual property double Period { double get(); }
};




For most types, this would be the extent of the work necessary to get the type working. With AnimationSet, however, we still need to account for inheritance, which complicates things greatly. First, we tackle the most straightforward task and implement ID3DXCompressedAnimationSet and ID3DXKeyframedAnimationSet:


public ref class CompressedAnimationSet : public AnimationSet
{
COMOBJECT(ID3DXCompressedAnimationSet, CompressedAnimationSet);

public:
CompressedAnimationSet( System::String^ name, double ticksPerSecond,
PlaybackType playbackType, DataStream^ compressedData,
array^ callbackKeys );
virtual ~CompressedAnimationSet() { }

static CompressedAnimationSet^ FromPointer( System::IntPtr pointer );

array^ GetCallbackKeys();
DataStream^ GetCompressedData();

property int CallbackKeyCount { int get(); }
property PlaybackType PlaybackType { SlimDX::Direct3D9::PlaybackType get(); }
property double SourceTicksPerSecond { double get(); }
};

public ref class KeyframedAnimationSet : public AnimationSet
{
COMOBJECT(ID3DXKeyframedAnimationSet, KeyframedAnimationSet);

public:
KeyframedAnimationSet( System::String^ name, double ticksPerSecond,
PlaybackType playbackType, int animationCount,
array^ callbackKeys );
virtual ~KeyframedAnimationSet() { }

static KeyframedAnimationSet^ FromPointer( System::IntPtr pointer );

DataStream^ Compress( float lossiness );
DataStream^ Compress( float lossiness, Frame^ frameHierarchy );

CallbackKey GetCallbackKey( int animation );
array^ GetCallbackKeys();
Result SetCallbackKey( int animation, CallbackKey callbackKey );

RotationKey GetRotationKey( int animation, int key );
array^ GetRotationKeys( int animation );
Result SetRotationKey( int animation, int key, RotationKey rotationKey );
Result UnregisterRotationKey( int animation, int key );
int GetRotationKeyCount( int animation );

ScaleKey GetScaleKey( int animation, int key );
array^ GetScaleKeys( int animation );
Result SetScaleKey( int animation, int key, ScaleKey scaleKey );
Result UnregisterScaleKey( int animation, int key );
int GetScaleKeyCount( int animation );

TranslationKey GetTranslationKey( int animation, int key );
array^ GetTranslationKeys( int animation );
Result SetTranslationKey( int animation, int key, TranslationKey translationKey );
Result UnregisterTranslationKey( int animation, int key );
int GetTranslationKeyCount( int animation );

int RegisterAnimationKeys( System::String^ name, array^ scaleKeys,
array^ rotationKeys, array^ translationKeys );
Result UnregisterAnimation( int animation );

property int CallbackKeyCount { int get(); }
property PlaybackType PlaybackType { SlimDX::Direct3D9::PlaybackType get(); }
property double SourceTicksPerSecond { double get(); }
};




That's a lot of work, but it's all just a straightforward wrapping of methods. Now we get to the interesting part. What happens when the user derives from AnimationSet and implements his own methods? CompressedAnimationSet and KeyframedAnimationSet have COM interfaces that DX knows about, so they get handled automatically. User defined types don't have this luxury.

To figure out how this needs to work, we need to look at how animation set gets used by DirectX. AnimationSet is only ever used by AnimationController, and it gets used in two ways. First, when it's getting passed into the controller, and second when it's getting returned from the controller. Since the animation sets are always passed into the controller before they can be retrieved from it (ie. the user always creates an animation set and registers it with the controller before they retrieve the animation set from the controller), we can tackle things from that direction.

So, the controller methods, such as RegisterAnimationSet, take an ID3DXAnimationSet as the parameter. Compressed and keyframed animation sets can be used with this method easily, since their interfaces derive from ID3DXAnimationSet. We need a way to handle this for user defined types as well. The solution? Derive a custom native class from ID3DXAnimationSet to act as a proxy wrapper for the managed type.


class AnimationShim : public ID3DXAnimationSet
{
private:
int refCount;
gcroot animationSet;
gcroot^> nameHandles;

public:
AnimationShim( AnimationSet^ animationSet );
virtual ~AnimationShim();
AnimationSet^ GetAnimationSet();

HRESULT WINAPI QueryInterface( const IID &iid, LPVOID *ppv );
ULONG WINAPI AddRef();
ULONG WINAPI Release();

HRESULT WINAPI GetAnimationIndexByName( LPCSTR Name, UINT *pIndex );
HRESULT WINAPI GetAnimationNameByIndex( UINT Index, LPCSTR *ppName );
HRESULT WINAPI GetCallback( DOUBLE Position, DWORD Flags, DOUBLE *pCallbackPosition, LPVOID *ppCallbackData );
LPCSTR WINAPI GetName();
UINT WINAPI GetNumAnimations();
DOUBLE WINAPI GetPeriod();
DOUBLE WINAPI GetPeriodicPosition( DOUBLE Position );
HRESULT WINAPI GetSRT( DOUBLE PeriodicPosition, UINT Animation, D3DXVECTOR3 *pScale, D3DXQUATERNION *pRotation, D3DXVECTOR3 *pTranslation );
};




As you can see, deriving from ID3DXAnimationSet isn't fun, and involves implementing IUnknown as well as the animation set methods. Further, since each native method calls into the managed method, we need to cache the returned strings and pin them so that they don't get moved while the native method plays with it. Since we can't know when they'll be used, we need to save the handle and release the pin in the destructor of the class. I use a dictionary here to avoid having the number of pin handles spiral out of control. This way, we only pin a given string once, and then refer to its handle whenever we need it again.

OK, so now we have a wrapper that can take any managed animation set and pass it into the native animation controller methods and have the methods called correctly. Further, since we store the managed animation set, when we retrieve an ID3DXAnimationSet from the animation controller, we can cast it to our custom type and retrieve the managed counterpart. Our type can now travel back and forth across the boundary at will.

This just shows one example of all the odd things that have to be dealt with when you're trying to play nice with both the managed and the unmanaged world, particularly COM. Things can get confusing and ugly fast, especially when you need to persist managed objects while a native method accesses it. Hopefully this has given you a taste of a real-world example of C++/CLI, and makes you think twice before attempting to use it without a lot of forethought.
Sign in to follow this  


3 Comments


Recommended Comments

Any idea how I can get a KeyframedAnimationSet object from an AnimationController's AnimationSets that have been loaded using Frame.LoadHierarchyFromX?

If your AnimationSet has been loaded from a .X file via Frame.LoadHierarchyFromX then there seems to be no way to get a KeyframedAnimationSet object from the method AnimationController.GetAnimationSet(int index). It always returns an InternalAnimationSet.

I also tried calling "KeyframedAnimationSet.FromPointer(animationSet.ComPointer)" on the animationSet returned from AnimationController.GetAnimationSet but this then throws an exception as the COM object representing the AnimationSet is already bound to a .NET object of type InternalAnimationSet and I can't see a way to change this binding:

Additional information: Unable to cast object of type 'SlimDX.Direct3D9.InternalAnimationSet' to type 'SlimDX.Direct3D9.KeyframedAnimationSet'.

Share this comment


Link to comment

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now