1

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

  1. Why does (Marker 1) even work? Does the content of the Owner class gets treated like a struct? If I try to change the struct AppleStack to a class and then passing instances without the ref it just copies everything to the native side.
  2. 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.

Paulsor
  • 100
  • 7
  • 1
    `...apple_buy([In, Out] ref AppleStack apple, int amount);` The [Out] attribute is required because the C++ modifies the struct, tells the pinvoke marshaler that it needs to copy the data back after the call. – Hans Passant Aug 25 '20 at 00:11
  • Your first test with `ref` is success but second test has no `ref` keyword. Are you using different signatures for the tests? – Louis Go Aug 25 '20 at 00:35
  • In the C# code, if you want it to match the C++ code, you need to declare `Owner` as `struct` rather than `class`. As it stands now, `Owner` is a reference type, but you want it to be a value type. – David Heffernan Aug 25 '20 at 07:52
  • BTW, in your C++ code, there is nothing to stop the `owner` field being allocated om the heap. A simple call to `new` will achieve that. – David Heffernan Aug 25 '20 at 07:53
  • @DavidHeffernan In C++ structs and classes are both allocated on the stack by default so this shouldn't be the reason for the problem here. – Paulsor Aug 25 '20 at 11:53
  • I understand perfectly well how structs and classes are allocated. I'm just correcting the error in your text where you said: "note that the owner field here is allocated on the stack, not the heap". This is why I said "BTW". I was not claiming that this was the reason for your problem. However, my other comment does identify one of the reasons why your code fails. – David Heffernan Aug 25 '20 at 12:25
  • @DavidHeffernan Ok then I misunderstood your comment, sorry for that. If I assign the owner on the side of C++ then it works on the native side, but it doesn't synchronize back to the C# struct. I'll update the question to show the changes I made based on all the feedback i already got. – Paulsor Aug 25 '20 at 12:39
  • You should do what I said in my first comment – David Heffernan Aug 25 '20 at 14:42
  • @DavidHeffernan Ok I declared `Owner` as a struct. The problem is that `apple2.owner = apple1.owner` now copies the owner. This means that these two stacks are owned by two separate instances. What I would like to achieve is that these two stacks can be owned by the same instance. I thought reference types would be the solution for this. – Paulsor Aug 25 '20 at 14:59
  • 1
    The C# marshaler doesn't support that. – David Heffernan Aug 25 '20 at 15:07

0 Answers0