MDX can still make me laugh

Published May 22, 2009
Advertisement
MDX has been dead for quite some time now, and these days most people recognize the futility of trying to use them for any new project. As a developer of SlimDX, I find myself still glancing at the internals of MDX from time to time, especially when contemplating how to wrap a particularly cumbersome portion of the DirectX API. That's what I was doing today when I ran across a real gem that I couldn't resist sharing with GDNet journal land.

DirectInput exposes an effect interface for dealing with force feedback effects. Each effect has a set of additional type-specific parameters, which turn out to be rather difficult to encapsulate correctly for consumption by managed code. Just as a quick primer, in native DirectInput IDirectInputEffect exposes a GetParameters method to get the effect parameters. The returned parameters structure contains a pointer to the extra type-specific parameters, along with a byte size. It is assumed that the C++ programmer will know the correct type for this data and then cast the pointer appropriately. In the managed world, this isn't so easy.

Ideally, the managed wrapper would construct the appropriate type and then return that to the user, as playing with pointers isn't quite kosher in the managed world. Unfortunately, the returned parameters contain no identifier to specify which type of data is being represented. I scratched my head at this one for a bit, and then took a look at how MDX handled the situation. I was quite surprised to find this little snippet of code hiding in the bowels of MDX:

internal static unsafe void ToManaged(ref Effect ret,     DIEFFECT modopt(IsConstModifier)* src){    if ((ret == 0) || (src == null))    {        throw new ArgumentNullException();    }    ret.m_Flags = *((EffectFlags*) (src + 4));    ret.m_Duration = *((int*) (src + 8));    ret.m_SamplePeriod = *((int*) (src + 12));    ret.m_Gain = *((int*) (src + 0x10));    ret.m_TriggerButton = *((int*) (src + 20));    ret.m_TriggerRepeatInterval = *((int*) (src + 0x18));    ret.m_StartDelay = *((int*) (src + 0x34));    if (*(((int*) (src + 0x20))) != 0)    {        int[] numArray2 = new int[*((int*) (src + 0x1c))];        numArray2.Initialize();        ret.m_Axes = numArray2;        volatile ref int pinned numRef2 = (volatile ref int) &(ret.m_Axes[0]);        memcpy(numRef2, *(((int*) (src + 0x20))), (*(((int*) (src + 0x1c)))             << 2));        numRef2 = 0;    }    if (*(((int*) (src + 0x24))) != 0)    {        int[] numArray = new int[*((int*) (src + 0x1c))];        numArray.Initialize();        ret.m_Direction = numArray;        volatile ref int pinned numRef = (volatile ref int)            &(ret.m_Direction[0]);        memcpy(numRef, *(((int*) (src + 0x24))), (*(((int*) (src + 0x1c)))             << 2));        numRef = 0;    }    uint num2 = *((uint*) (src + 40));    if ((num2 != 0) && (((num2[4] != 0) || (num2[8] != 0)) ||         ((num2[12] != 0) || (num2[0x10] != 0))))    {        ret.m_UsesEnvelope = true;        Envelope.ToManaged(ref ret.EnvelopeStruct, *((DIENVELOPE**)            (src + 40)));    }    if (0x10 == *(((int*) (src + 0x2c))))    {        DICUSTOMFORCE* dicustomforcePtr = *((DICUSTOMFORCE**) (src + 0x30));        int num6 = *(((int*) (dicustomforcePtr + 8))) << 2;        if (((IsBadReadPtr(*((void modopt(IsConstModifier)**) (dicustomforcePtr + 12)), (uint) num6) == 0)               && (IsBadWritePtr(*((void**) (dicustomforcePtr + 12)),                   (uint) num6) == 0)) &&              (*(((int*) (dicustomforcePtr + 8))) > 0))        {            CustomForce.ToManaged(ref ret.CustomStruct, dicustomforcePtr);            ret.m_EffType = EffectType.CustomForce;            return;        }    }    uint num = *((uint*) (src + 0x2c));    if (0x10 == num)    {        Periodic.ToManaged(ref ret.Periodic, *((DIPERIODIC**) (src + 0x30)));        ret.m_EffType = EffectType.Periodic;    }    else if (4 == num)    {        ConstantForce.ToManaged(ref ret.Constant, *((DICONSTANTFORCE**) (src + 0x30)));        ret.m_EffType = EffectType.ConstantForce;    }    else if (8 == num)    {        RampForce.ToManaged(ref ret.RampStruct, *((DIRAMPFORCE**) (src + 0x30)));        ret.m_EffType = EffectType.RampForce;    }    else if ((num != 0) && ((num % 0x18) == 0))    {        int num5 = (int) (num / 0x18);        Condition[] conditionArray = new Condition[num5];        conditionArray.Initialize();        ret.ConditionStruct = conditionArray;        ret.m_EffType = EffectType.Condition;        int index = 0;        if (0 < num5)        {            DIEFFECT modopt(IsConstModifier)* dieffectPtr = src + 0x30;            int num4 = 0;            do            {                Condition.ToManaged(ref ret.ConditionStruct[index], (DICONDITION*) (num4 + *(((int*) dieffectPtr))));                index++;                num4 += 0x18;            }            while (index < num5);        }    }}


Yes, it's ugly, but a lot of that is due to the compiler having stripped out constant and structure accesses. That's not really the point here though. What's really crazy is how this snippet of code is determining the correct type for the extra type-specific parameters. Let's look more closely at the last portion of this method, cleaned up to remove compiler funkiness.

if (effect.Size == 16){    DICUSTOMFORCE* ptr = (DICUSTOMFORCE*)effect.TypeSpecificParams;    if (!IsBadReadPtr(ptr->ForceData) && !IsBadWritePtr(ptr->ForceData))        return CustomForce.ToManaged(ptr);}if (effect.Size == 16)    return PeriodicForce.ToManaged(effect.TypeSpecificParams);else if (effect.Size == 4)    return ConstantForce.ToManaged(effect.TypeSpecificParams);else if (effect.Size == 8)    return RampForce.ToManaged(effect.TypeSpecificParams);else if (effect.Size != 0 && (effect.Size % 24) == 0)    return ConditionArray.ToManaged(effect.TypeSpecificParams);


In case you don't quite understand the hilariousness that is this code, let me give you a little guidance. First, the function checks the number of bytes returned, and tries to match it up against the size of possible type-specific structures. As you can see, there are four possible force types. If the size doesn't match any of those, it checks to see if it's aligned on a Condition array boundary, in which case it interprets the data as an array of Condition structures. That's fairly hackish on it's own, but as you can see by that first if block, it gets better.

Unfortunately for the original MDX developers, both DIPERIODIC and DICUSTOMFORCE both have a size of 16 bytes, so how would you know which structure was being returned? Simple! Since DIPERIODIC contains four integers, and DICUSTOMFORCE contains three integers and a pointer, simply take a guess to see if the data at the correct offset could possibly be considered a valid pointer. If so, we'll pretend that we have a DICUSTOMFORCE and everything will be hunky dory!

The only question is, how do you know if a given set of bytes represents a pointer? Simple! Use the handy-dandy IsBadReadPtr and IsBadWritePtr functions to figure it out for you. For those of you in the know, IsBadXxxPtr should really be called CrashProgramRandomly or perhaps CorruptMemoryIfPossible. I haven't done much research on these two little functions, but before I'd even read the aforementioned article, I had already guessed that they weren't quite standard C++, if you catch my drift.

Now, this little... what did I call it... gem? that I've found here is quite out of scope for most normal applications, but I can't help but wonder if anyone had ever attempted to use this bit of functionality and found it horribly broken.
Previous Entry Combinatorics
0 likes 5 comments

Comments

LachlanL
Wow.... I hope that was some summer intern or something. I find the possibility that the same people wrote the OS I'm currently using.... scary.
May 25, 2009 02:36 AM
remigius
Quote:Original post by LachlanL
Wow.... I hope that was some summer intern or something. I find the possibility that the same people wrote the OS I'm currently using.... scary.


As I understand MDX back in the day was pretty much a one-man show. Tom Miller wrote the wrapper, 2 rewrites (1.0 to 1.1, to actually use properties, and the epic 2.0 timebomb) and 2 books on the subject. With those limited resources and a bit of odd prioritization there's bound to be some problems, especially in the more obscure fields of the API, but that goes for DX itself too.

I've certainly had my share of troubles with the API, but overall it wasn't that bad. It opened up DirectX to us managed folks and it kicked off both the XNA and SlimDX initiatives. I think that's commendable at the very least [smile]
May 25, 2009 03:53 AM
Evil Steve
I just died a little more inside [sad]
May 25, 2009 04:07 AM
Wan
Nice find. Deliciously horrible.
June 11, 2009 12:24 PM
x4000
I successfully used the force feedback effects in MDX, but never this particular GetParameters method that I recall. I can't believe something like this was lurking under the hood the whole time. I mean, what a potential timebomb if I had happened to use that particular method.
June 17, 2009 08:10 AM
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Advertisement

Latest Entries

New Blog

2021 views

Progress Update

1550 views

Start of Project

1513 views

New Job

2220 views

The Downward Spiral

2878 views

Job Interviews

1479 views
Advertisement