I tried two days to pass an instance of a struct containing an instance of a class on the C#-side (.NET Core 3.1 if that matters) to C++ via PInvoke. I will explain my questions and problems as I show the sample code. I really don't know what I'm missing here.
Relevant C# code
As you can see I have an AppleStack
class which holds a reference to an instance of Owner
class so one person can own multiple stacks of apples. Various AppleStack
instances should be passable to the C++ code as a reference so I can modify their fields within native code:
[StructLayout(LayoutKind.Sequential)]
class Owner
{
public int id;
public float money;
}
[StructLayout(LayoutKind.Sequential)]
struct AppleStack
{
public int stock;
public float price;
public Owner owner;
}
I obviously need to link the function from both "worlds" together:
[DllImport("NativeDll.dll", CallingConvention = CallingConvention.Cdecl)]
private extern static void apple_buy(ref AppleStack apple, int amount);
[DllImport("NativeDll.dll", CallingConvention = CallingConvention.Cdecl)]
private extern static void apple_print(ref AppleStack apple);
So far so good. Now I instantiated two stacks of apples sharing the same owner. The specific values don't matter but I can provide this snippet if necessary. After the instantiation, I call the C++ functions in this order:
apple_print(ref apple1);
apple_buy(ref apple1, 3);
apple_buy(ref apple1, 2);
apple_print(ref apple1);
apple_print(ref apple2);
Relevant C++ code
On the native side, I have these two matching types:
struct Owner
{
int id;
float money;
};
struct Apple
{
int stock;
float price;
Owner owner; // (Marker 1)
};
Note that the owner field here is allocated on the stack, not the heap. This will be relevant to my questions below. Before I will show the buy method. The print method only logs the fields so I won't put it here, but of course, I can provide it if it's desired:
void apple_buy(Apple* apple, int amount)
{
std::cout << "You bought apples (x" << amount << ") from " << apple->owner.id
<< " for " << (amount * apple->price) << "$!" << std::endl;
apple->stock -= amount;
apple->owner.money += amount * apple->price;
}
Console output
Stack of 7 apples for 2.1$ found!
Owner has id 10 and owns 225.53$ of money.
You bought apples (x3) from 10 for 0.9$!
You bought apples (x2) from 10 for 0.6$!
Stack of 2 apples for 0.6$ found!
Owner has id 10 and owns 227.03$ of money.
Stack of 11 apples for 4.4$ found!
Owner has id 10 and owns 225.53$ of money.
The last two lines came from the second stack. All other lines came from the first stack. Edit: After the purchases the owner of the two stacks should own the same money but after those purchases he has 225.53$ and 227.03$ at the same time.
An alternative solution I tried
I've read from this question (also on StackOverflow) that you could do this with the GCHandle
class. So I changed, for example, the apple_print
method to private extern static void apple_print(IntPtr apple);
. The main method will then end like this:
var handle1 = GCHandle.Alloc(apple1);
var handle2 = GCHandle.Alloc(apple2);
apple_print(GCHandle.ToIntPtr(handle1));
apple_buy(GCHandle.ToIntPtr(handle1), 3);
apple_buy(GCHandle.ToIntPtr(handle1), 2);
apple_print(GCHandle.ToIntPtr(handle1));
apple_print(GCHandle.ToIntPtr(handle2));
But this results in complete nonsense and doesn't even solve the problem if I look close at the output:
Stack of -2147424088 apples for -1.76037e-33$ found!
Owner has id -2147428072 and owns 8.1976e-43$ of money.
You bought apples (x3) from -2147428072 for 2.45928e-42$!
You bought apples (x2) from -2147428072 for 1.63952e-42$!
Stack of -2147424093 apples for -1.76037e-33$ found!
Owner has id -2147428072 and owns 4.91856e-42$ of money.
Stack of -2147424032 apples for -1.76037e-33$ found!
Owner has id -2147424093 and owns 8.1976e-43$ of money.
Questions
- Why does (Marker 1) even work? Does the content of the
Owner
class gets treated like a struct? If I try to change thestruct AppleStack
to a class and then passing instances without the ref it just copies everything to the native side. - The given example does work but for an obvious reason, the owner gets copied to the second stack instead of being shared as a reference. What's the best practice to tackle this?
- If GCHandle is the answer whats the point I'm not getting to understand this?
If you need more information I will try to provide it as fast as possible. I thank everyone in advance who tries to help here :D
Edit 1
Based on the feedback i received i made several changes to the code. First i set the attributes
[DllImport("NativeDll.dll", CallingConvention = CallingConvention.Cdecl)]
private extern static void apple_buy([In, Out] ref AppleStack apple, int amount);
in fornt of the parameter. Then, instead of allocating the owner on managed side I assign null
to the fields. On the native side I changed the print function to
void apple_print(Apple* apple)
{
static Owner owner{ 10, 225.0f };
apple->owner = &owner;
// Printing fields...
}
(a bit hacky i know but it will do its' work for this example i hope) as well as the struct to
struct Apple
{
int stock;
float price;
Owner* owner;
};
which works great as long as I don't access the field within .NET Core. Then the output (via Console.WriteLine($"Owner has: {apple1.owner.money}$");
) is
Stack of 7 apples for 2.1$ found!
Owner has id 10 and owns 225$ of money.
You bought apples (x3) from 10 for 0.9$!
You bought apples (x2) from 10 for 0.6$!
Stack of 2 apples for 0.6$ found!
Owner has id 10 and owns 226.5$ of money.
Stack of 11 apples for 4.4$ found!
Owner has id 10 and owns 226.5$ of money.
Owner has: 4,5912E-41$
so the owner exists but the data isn't interpreted correctly. My goal is that I can just pass the instance as is for the best possible performance. I need to maintain the one to many relationship somehow.