Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

PInvokeStackImbalance Not Caused By CallingConvention

Tags:

c

c#

pinvoke

I have no idea what the problem is here.

I have a ton of p/invoke calls that are working without incident... except this one.

I've managed to reduce my problem to the following sample code.

If I remove either struct member (either the double or the int) it works fine.

I'm assuming the problem is somehow related to the layout of the struct - but when I do a sizeof() in C and a Marshal.SizeOf() in C#, they both return the same value... so if the struct size is the same in C# and C, what could the problem be?

I'm obviously missing something basic here.

SampleDLLCode.c

#pragma pack(1)

typedef struct SampleStruct {
    double structValueOne;
    int structValueTwo;
} SampleStruct;

__declspec(dllexport) SampleStruct __cdecl SampleMethod(void);
SampleStruct SampleMethod(void) { 
    return (SampleStruct) { 1, 2 };
}

Build Script

gcc -std=c99 -pedantic -O0 -c -o SampleDLLCode.o SampleDLLCode.c
gcc -shared --out-implib -o SampleDLL.dll SampleDLLCode.o 

C# Code

using System;
using System.Runtime.InteropServices;

namespace SampleApplication
{
    [StructLayout(LayoutKind.Sequential, Pack=1)]
    public struct SampleStruct {
        public double structValueOne;
        public int structValueTwo;
    } 

    class Program
    {
        [DllImport("SampleDLL.dll", CallingConvention = CallingConvention.Cdecl)]
        public static extern SampleStruct SampleMethod();

        static void Main(string[] args)
        {
            SampleStruct sample = SampleMethod();
        }
    }
}
like image 842
Steve Avatar asked Mar 24 '13 07:03

Steve


1 Answers

First of all let me congratulate you on a very well asked question. It was a delight, for once, to receive all the code that was needed to reproduce the problem.

The problem is due to the slightly different ABIs used by gcc and Microsoft tools for function return values. For return values that can fit into registers, for example int return values there are no differences. But since your struct is too large to fit in a single register and there are differences between the APIs in that situation.

For larger return values, the caller passes a hidden pointer to the function. This hidden pointer is pushed onto the stack by the caller. The function writes the return value to the memory address specified by that hidden pointer. The difference in the ABIs is in who pops that hidden pointer off the stack. The Microsoft tools use an ABI that requires the caller to pop the hidden pointer, but the default gcc ABI asks the callee to do that.

Now, gcc being almost infinitely configurable, there is a switch that will allow you to control the ABI. And you can make gcc use the same rules as the Microsoft tools. Doing so requires the callee_pop_aggregate_return function attribute.

Change your C code to be like this:

__declspec(dllexport) SampleStruct __cdecl SampleMethod(void) 
    __attribute__((callee_pop_aggregate_return(0)));
    // specifies that caller is responsible for popping the hidden pointer

SampleStruct SampleMethod(void) { 
    return (SampleStruct) { 1, 2 };
}
like image 80
David Heffernan Avatar answered Nov 08 '22 15:11

David Heffernan