views:

1203

answers:

5

So I have a native 3rd party C++ code base I am working with (.lib and .hpp files) that I used to build a wrapper in C++/CLI for eventual use in C#.

I've run into a particular problem when switching from Debug to Release mode, in that I get an Access Violation Exception when a callback's code returns.

The code from the original hpp files for callback function format:

typedef int (*CallbackFunction) (void *inst, const void *data);

Code from the C++/CLI Wrapper for callback function format: (I'll explain why I declared two in a moment)

public delegate int ManagedCallbackFunction (IntPtr oInst, const IntPtr oData);
public delegate int UnManagedCallbackFunction (void* inst, const void* data);

--Quickly, the reason I declared a second "UnManagedCallbackFunction" is that I tried to create an "intermediary" callback in the wrapper, so the chain changed from Native C++ > C# to a version of Native C++ > C++/CLI Wrapper > C#...Full disclosure, the problem still lives, it's just been pushed to the C++/CLI Wrapper now on the same line (the return).

And finally, the crashing code from C#:

public static int hReceiveLogEvent(IntPtr pInstance, IntPtr pData)
    {
        Console.WriteLine("in hReceiveLogEvent...");
        Console.WriteLine("pInstance: {0}", pInstance);
        Console.WriteLine("pData: {0}", pData);

        // provide object context for static member function
        helloworld hw = (helloworld)GCHandle.FromIntPtr(pInstance).Target;
        if (hw == null || pData == null)
        {
            Console.WriteLine("hReceiveLogEvent: received null instance pointer or null data\n");
            return 0;
        }

        // typecast data to DataLogger object ptr
        IntPtr ip2 = GCHandle.ToIntPtr(GCHandle.Alloc(new DataLoggerWrap(pData)));
        DataLoggerWrap dlw = (DataLoggerWrap)GCHandle.FromIntPtr(ip2).Target;

        //Do Logging Stuff

        Console.WriteLine("exiting hReceiveLogEvent...");
        Console.WriteLine("pInstance: {0}", pInstance);
        Console.WriteLine("pData: {0}", pData);
        Console.WriteLine("Setting pData to zero...");
        pData = IntPtr.Zero;
        pInstance = IntPtr.Zero;
        Console.WriteLine("pData: {0}", pData);
        Console.WriteLine("pInstance: {0}", pInstance);

        return 1;
    }

All writes to the console are done and then we see the dreaded crash on the return:

Unhandled exception at 0x04d1004c in helloworld.exe: 0xC0000005: Access violation reading location 0x04d1004c.

If I step into the debugger from here, all I see is that the last entry on the call stack is: > "04d1004c()" which evaluates to a decimal value of: 80805964

Which is only interesting if you look at the console which shows:

entering registerDataLogger
pointer to callback handle: 790848
fp for callback: 2631370
pointer to inst: 790844
in hReceiveLogEvent...
pInstance: 790844
pData: 80805964
exiting hReceiveLogEvent...
pInstance: 790844
pData: 80805964
Setting pData to zero...
pData: 0
pInstance: 0

Now, I know that between debug and release some things are quite different in the Microsoft world. I am, of course worried about byte padding and initialization of variables, so if there is something I am not providing here, just let me know and I'll add to the (already long) post. I also think the managed code may NOT be releasing all ownership and then the native C++ stuff (which I don't have the code for) may be trying to delete or kill off the pData object, thus crashing the app.

More full disclosure, it all works fine (seemingly) in Debug mode!

A real head scratch issue that would appreciate any help!

A: 

This doesn't directly answer your question, but it may lead you in the right direction as far as debug mode okay vs. release mode not okay:

Since the debugger adds a lot of record-keeping information to the stack, generally padding out the size and layout of my program in memory, I was “getting lucky” in debug mode by scribbling over 912 bytes of memory that weren’t very important. Without the debugger, though, I was scribbling on top of rather important things, eventually walking outside of my own memory space, causing Interop to delete memory it didn’t own.

What is the definition of DataLoggerWrap? A char field may be too small for the data you are receiving.

cfeduke
+1  A: 

I think the stack got crushed because of mismatching calling conventions: try out to put the attribute

 [UnmanagedFunctionPointer(CallingConvention.Cdecl)]

on the callback delegate declaration.

jdehaan
For support, this was mostly right. After contacting the 3rd party vendor, we found out they compiled using cdecl spec and not the stdcall needed for Managed Code Compliance: http://msdn.microsoft.com/en-us/library/367eeye0%28VS.80%29.aspx. I added a question on StackOverflow to ask why does this need to be so? Hopefully, someone will give a better explanation than the referenced MSDN article.
TomO
In the project settings there is a default for the calling convention used (C/C++) if not specified with __declspec(). This calling convention was not visible in the code. What happens with mismatching conventions is clear: if the responsibility for stack cleanup is not matching, it crushes the stack (not reset to its state before the call because of either a double cleanup or too few). This depends on the amount of arguments passed on the stack. http://en.wikipedia.org/wiki/Calling_convention
jdehaan
A: 

I'm not sure what your are trying to achieve.

A few points:

1) The garbage collector is more aggressive in release mode so with bad ownership the behaviour you describe is not uncommon.

2) I don't understands what the below code is trying to do?

IntPtr ip2 = GCHandle.ToIntPtr(GCHandle.Alloc(new DataLoggerWrap(pData)));
DataLoggerWrap dlw = (DataLoggerWrap)GCHandle.FromIntPtr(ip2).Target;

You use GCHandle.Alloc to lock an instance of DataLoggerWrap in memory, but then you never pass it out to unmanaged - so why do you lock it? You also never free it?

The second line then grabs back a reference - why the circular path? why the reference - you never use it?

3) You set the IntPtrs to null - why? - this will have no effect outside of the function scope.

4) You need to know what the contract of the callback is. Who owns pData the callback or the calling function?

morechilli
A: 

I'm with @jdehaan, except CallingConvetion.StdCall could be the answer, especially when the 3rd party lib is written in BC++, for example.

Mike Jiang
A: 

You guys are super. I have the exact problem. By forcing the calling convention in the c++ code, the problem is gone. Thanks

ch