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
- Why is exception is thrown when struct has 2 uints but not when it has 3 uints?
- C# doesn't know the unmanaged function prototype right? If so, why when the struct is large and I pass a wrong parameter (e.g. unmanaged function expects pointer but I pass struct or expects struct but I pass reference) the code is working and no exception is thrown.
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");
}
}
}