Reputation: 63
I am working on a C# .NET wrapper that wraps an unmanaged C++ Driver. The logic is to create a C# class that has methods which wrap the DLLImport
entries.
One of the functions of the unmanaged driver expects pointer to a struct. I know that the correct way to pass the struct is by ref because in C# structures are value types Structure types (C# reference) and since C# is always passing the value it would pass the managed address of the struct while automatically pinning and marshalling (correct this if I am wrong)
But for experimentation I didn't passed a ref
but instead the whole struct and I was expecting to get an exception like memory access denied due to the unmanaged driver would manipulate the parameter as pointer.
I found that this behavior is not the same when the struct is smaller. For example when the struct has only 2 uint
fields (before had 3 uints) then I get the following exception
System.AccessViolationException: 'Attempted to read or write protected memory. This is often an indication that other memory is corrupt.'
Probably due to compiler optimization if the struct size does not exceed some threshold then it manipulates the struct by value and the above exception is thrown, while if the struct exceed the threshold it manipulates is as reference and then no exception is thrown.
I didn't find any documentation that states this behavior.
So this got me confused and created some questions. If anyone can answer them it would be very helpful to understand how automatic marshaling is working
I will post an example of wrapper/driver projects that I used for testing in case anyone want to try. You can remove the ref
from the second parameter of foo
and check the results with different struct sizes.
C++ Driver
Datatypes.h
#pragma once
#include"basetsd.h"
typedef struct _TestStruct
{
UINT32 x;
UINT32 y;
UINT32 z;
} TestStruct;
main.cpp
#include<iostream>
#include"main.h"
#include"basetsd.h"
#include"Datatypes.h"
using namespace std;
int foo(TestStruct p, TestStruct *p2)
{
p2->x = 10;
p.x = 11;
cout << "p address 0x" << &p << endl;
cout << "p2 value 0x" << &(*p2) << endl;
cout << "p2 address 0x" << &p2 << endl;
while (1);
return 0;
}
main.h
#pragma once
#include"Datatypes.h"
#ifdef _EXPORTING
#define DECLSPEC __declspec(dllexport)
#else
#define DECLSPEC __declspec(dllimport)
#endif
#ifdef __cplusplus
extern "C" { // only need to export C interface if
// used by C++ source code
#endif
DECLSPEC int foo(TestStruct p, TestStruct *p2);
#ifdef __cplusplus
}
#endif
C# Wrapper
wrapper.cs
using System.Runtime.InteropServices;
namespace Wrapper
{
public struct WTestStruct
{
public uint x;
public uint y;
public uint z;
public uint c;
}
internal class wrapper
{
[DllImport("Driver.dll", EntryPoint = "foo", CallingConvention=CallingConvention.Cdecl)]
public static extern int Foo(WTestStruct p, ref WTestStruct p2);
}
}
main.cs
using System;
namespace Wrapper
{
class main
{
static void Main()
{
WTestStruct p = new WTestStruct();
p.x = 1;
p.y = 2;
p.z = 3;
p.c = 4;
wrapper.Foo(p, ref p);
Console.WriteLine("x: {0}", p.x);
Console.WriteLine("Executed");
}
}
}
Upvotes: 1
Views: 76
Reputation: 63
First of all thanks for the response, it helped a lot.
Of course as @Hans Passant said
Bad declarations may seem to work by accident
So this is definitely not an issue on the marshaling or on .NET part, but it is an issue on the developer side. Beside this is well documented in .NET
The Rect value type must be passed by reference because the unmanaged API is expecting a pointer to a RECT to be passed to the function - Default marshalling for value types
But lets continue with the investigation.
The caller then allocates space for the struct and copies the value into it. And passes a pointer to that copy, making the distinction disappear.
So if I understand correctly when the struct is "large" the managed code will pass the address even if the parameter is not defined with 'ref'. But I think this would only work if the following is correct.
Since P/Invoke doesn't know if the unmanaged function expects pointer or struct it will place the struct on the stack and the address will be passed with the registers (and not with the stack to not mess with the alignment). Therefore if the unmanaged function wants the value then it will look at the stack else if it wants the pointer it will look at the registers.
After some testing I found out using the posted wrapper and driver that indeed the RCX register contains an address on stack and that address when looking at the Memory window on Visual Studio has the values 1, 2, 3 and 4 with 4 bytes each so I assume this is the struct.
Thanks again for the help
Upvotes: 0