0

I have a C# project which uses C++ classes from a library. C# classes are actually wrappers for C++ classes, they expose C++ functionality to C# client code. In many places C++ value classes are converted to C# wrappers and backwards. During code review I've found two ways how the classes are converted: via reinterpret_cast ( see operator * ) and via pin_ptr ( see MultiplyBy ); As you can see, both native and managed class has three 'double' fields, this is why someone was using reinterpret_cast;

In many places classes are copied from C# to C++ using memcpy: memcpy(&NativePointInstance, &ManagedPointIntance, sizeof(double)*3);

I've heard from one developer that reinterpret_cast can be safe in some cases, when we work with C# value classes.

The question is: When it is safe to use reinterpret_cast on C# value classes and when it is not? What is the most correct way of converting the pointers in this case - like in operator * or like in MultiplyBy, or another alternative?

Can someone explain in details what is happening in MultiplyBy(), how these trick work?

As far as I understood, the issue may be caused by that optimizer may change fields order, GC may reorganize heap, and alignment of fields may be different between managed and native code.

// this is C++ native class
class NativePoint
{
public:
  double x;
  double y;
  double z;
  NativePoint(double x, double y, double z)
  {
    this->x = x;
    this->y = y;
    this->z = z;
  }
  NativePoint operator * (int value)
  {
    return NativePoint(x * value, y * value, z * value);
  }
};

// this class managed C++ class
[StructLayout(LayoutKind::Sequential)]
public value class ManagedPoint
{
internal:
  double x;
  double y;
  double z;
  ManagedPoint(const NativePoint& p)
  {
    x = p.x;
    y = p.y;
    z = p.z;
  }
public:
  static ManagedPoint operator * (ManagedPoint a, double value)
  {
    return ManagedPoint((*reinterpret_cast<NativePoint*>(&(a))) * value);
  }
  ManagedPoint MultiplyBy(double value)
  {
    pin_ptr<ManagedPoint> pThisTmp = &*this;
    NativePoint* pThis = reinterpret_cast<NativePoint*>(&*pThisTmp);
    return ManagedPoint(*pThis * value);
  }
};

// this should be called from C# code, or another .NET app
int main(array<System::String ^> ^args)
{
  NativePoint p_native = NativePoint(1, 1, 1);
  ManagedPoint p = ManagedPoint(p_native);
  Console::WriteLine("p is {" + p.x + ", " + p.y + ", " + p.z + "}");
  ManagedPoint p1 = p * 5;
  Console::WriteLine("p1 is {" + p1.x + ", " + p1.y + ", " + p1.z + "}");
  ManagedPoint p2 = p.MultiplyBy(5);
  Console::WriteLine("p2 is {" + p2.x + ", " + p2.y + ", " + p2.z + "}");
  Console::ReadLine();
  return 0;
}
  • "As far as I understood, the issue may be caused by" I'm confused. What is your issue? The rest of your post seems to ask about best practices with reinterpret cast, and then you say you have an issue. Can you maybe give more detail of the issue, or maybe create separate questions, one for the issue and one for safe practices? –  May 29 '19 at 17:07
  • reinterpret cast is never safe, in the sense that you have to make sure it is the right thing to do. Did you read about what is allowed and what not, eg here https://en.cppreference.com/w/cpp/language/reinterpret_cast ? – 463035818_is_not_an_ai May 29 '19 at 17:26
  • It is never safe, the C++/CLI compiler is forbidden from doing this by itself. Memory layout of managed types is fundamentally undiscoverable. Using memcpy() is something you'll get away with here, the CLR has no great reasons to alter the layout. But it is actually inefficient, it can't beat assigning those three fields explicitly. Marshal::PtrToStructure() is safe. – Hans Passant May 29 '19 at 17:27
  • Scary example [is here](https://stackoverflow.com/questions/24742325/why-does-struct-alignment-depend-on-whether-a-field-type-is-primitive-or-user-de). – Hans Passant May 29 '19 at 17:32

1 Answers1

0

Well, I have ended up using usual constructors of native classes. It looks for me absolutely safe, and fastest from remaining variants. The idea from comments with Marshal::PtrToStructure() was good, but for my test example gave slower execution, than the same with using constructors. Pointer casts are the fastest solution, but after very scary example from comments, I will not risk to use it anymore (except if we really need to optimize it, then LayoutKind::Explicit should do the thing).

Here is code i used for testing:

// this is C++ native class
class NativePoint
{
public:
  double x;
  double y;
  double z;
  NativePoint()
  {

  }
  NativePoint(double x, double y, double z)
  {
    this->x = x;
    this->y = y;
    this->z = z;
  }
  NativePoint operator * (int value)
  {
    return NativePoint(x * value, y * value, z * value);
  }
};

// this class managed C++ class
[StructLayout(LayoutKind::Sequential)]
public value class ManagedPoint
{
internal:
  double x;
  double y;
  double z;
  ManagedPoint(const NativePoint& p)
  {
    x = p.x;
    y = p.y;
    z = p.z;
  }
  ManagedPoint(double x, double y, double z)
  {
    this->x = x;
    this->y = y;
    this->z = z;
  }
public:
  static ManagedPoint operator * (ManagedPoint a, double value)
  {
    return ManagedPoint((*reinterpret_cast<NativePoint*>(&(a))) * value);
  }
  ManagedPoint MultiplyBy(double value)
  {
    pin_ptr<ManagedPoint> pThisTmp = &*this;
    NativePoint* pThis = reinterpret_cast<NativePoint*>(&*pThisTmp);
    return ManagedPoint(*pThis * value);
  }
};

// this class managed C++ class
[StructLayout(LayoutKind::Sequential)]
public value class ManagedPoint2
{
internal:
  double x;
  double y;
  double z;
  ManagedPoint2(const NativePoint& p)
  {
    x = p.x;
    y = p.y;
    z = p.z;
  }
  ManagedPoint2(double x, double y, double z)
  {
    this->x = x;
    this->y = y;
    this->z = z;
  }
public:
  static ManagedPoint2 operator * (ManagedPoint2 a, double value)
  {
    return ManagedPoint2((NativePoint(a.x, a.y, a.z)) * value);
  }
  ManagedPoint2 MultiplyBy(double value)
  {
    return ManagedPoint2((NativePoint(this->x, this->y, this->z)) * value);
  }
};

// this class managed C++ class
[StructLayout(LayoutKind::Sequential)]
public value class ManagedPoint3
{
internal:
  double x;
  double y;
  double z;
  ManagedPoint3(const NativePoint& p)
  {
    x = p.x;
    y = p.y;
    z = p.z;
  }
  ManagedPoint3(double x, double y, double z)
  {
    this->x = x;
    this->y = y;
    this->z = z;
  }
public:
  static ManagedPoint3 operator * (ManagedPoint3 a, double value)
  {
    NativePoint p;
    Marshal::StructureToPtr(a, IntPtr(&p), false);
    return ManagedPoint3(p * value);
  }
  ManagedPoint3 MultiplyBy(double value)
  {
    NativePoint p;
    Marshal::StructureToPtr(*this, IntPtr(&p), false);
    return ManagedPoint3(p * value);
  }
};

// this class managed C++ class
[StructLayout(LayoutKind::Sequential)]
public value class ManagedPoint4
{
internal:
  double x;
  double y;
  double z;
  ManagedPoint4(const NativePoint& p)
  {
    x = p.x;
    y = p.y;
    z = p.z;
  }
  ManagedPoint4(double x, double y, double z)
  {
    this->x = x;
    this->y = y;
    this->z = z;
  }
public:
  static ManagedPoint4 operator * (ManagedPoint4 a, double value)
  {
    return ManagedPoint4(ManagedPoint4::ToNative(a) * value);
  }
  ManagedPoint4 MultiplyBy(double value)
  {
    return ManagedPoint4(ManagedPoint4::ToNative(*this) * value);
  }
  static NativePoint ToNative(const ManagedPoint4& pp)
  {
    NativePoint p;
    Marshal::StructureToPtr(pp, IntPtr(&p), false);
    return p;
  }
};

// this should be called from C# code, or another .NET app
int main(array<System::String ^> ^args)
{
  Stopwatch time;
  time.Start();
  for (int i = 0; i < 10000000; i++)
  {
    ManagedPoint a = ManagedPoint(1, 2, 3) * 4;
  }
  time.Stop();
  Console::WriteLine("time: " + time.ElapsedMilliseconds);

  Stopwatch time2;
  time2.Start();
  for (int i = 0; i < 10000000; i++)
  {
    ManagedPoint2 a2 = ManagedPoint2(1, 2, 3) * 4;
  }
  time2.Stop();
  Console::WriteLine("time2: " + time2.ElapsedMilliseconds);

  Stopwatch time3;
  time3.Start();
  for (int i = 0; i < 10000000; i++)
  {
    ManagedPoint3 a3 = ManagedPoint3(1, 2, 3) * 4;
  }
  time3.Stop();
  Console::WriteLine("time3: " + time3.ElapsedMilliseconds);

  Stopwatch time4;
  time4.Start();
  for (int i = 0; i < 10000000; i++)
  {
    ManagedPoint4 a3 = ManagedPoint4(1, 2, 3) * 4;
  }
  time4.Stop();
  Console::WriteLine("time4: " + time4.ElapsedMilliseconds);

  Console::ReadLine();
  Console::WriteLine("======================================================");
  Console::WriteLine();

  return 0;
}

And this is output:

time: 374
time2: 382
time3: 857
time4: 961

time: 395
time2: 413
time3: 900
time4: 968

time: 376
time2: 378
time3: 840
time4: 909