Search code examples
c++.netreinterpret-castpin-ptr

Managed to native value class conversion: is it safe to cast pointer?


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;
}

Solution

  • 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