2

I am trying to understand this "IT JUST WORKS" magic for C#/C++ interop, but currently IT'S JUST A NIGHTMARE.

I am playing with Mandelbrot computations and want to offload the computing core to native C++ and SSE2. This is working, with P/Invoke. Now I want to change to IJW, to be more typesafe and because I want to understand it. But it was decades ago when I scratched at the surface of C++.

I have a struct Complex { double real; double imag; } to hold the start values for the Mandelbrot loop, and I'd like to call a function like this:

Compute(int vectorSize, Complex[] points, double maxValue, int maxLoops, int[] result)

Now I created a CLR Class Library with VS Express 2013 and put this into a Header file:

public value struct Complex
{
    double real;
    double imag;
};

public ref class Computations
{
public:
    static void Basic(int vectorSize, array<Complex,1>^ points, double maxRadius, int maxLoops, array<int,1>^ result);
};

class NativeComputations
{
public:
    static void Basic(int vectorSize, Complex* points, double maxRadius, int maxLoops, int* result);
};

and in the CPP file this:

#pragma managed
void Mandelbrot::Computations::Basic(int vectorSize, array<Complex,1>^ points, double maxRadius, int maxLoops, array<int,1>^ result)
{
    pin_ptr<Complex> pPoints = &points[0];
    pin_ptr<int> pResult = &result[0];
    NativeComputations::Basic(vectorSize, pPoints, maxRadius, maxLoops, pResult);
}

#pragma unmanaged
void Mandelbrot::NativeComputations::Basic(int vectorSize, Complex* points, double maxRadius, int maxLoops, int* result)
{
    double foo = points[0].real;
}

At this point I am stuck - error C3821: 'points': managed type or function cannot be used in an unmanaged function

So I need to use something unmanaged. I can repeat my code and declare a ComplexNative struct (by omitting the "value" keyword). This is feasible, but repeat code? And even if this is the way, what is necessary to translate the Complex[] to a pinned ComplexNative*?

And please, I don't want to split the struct into a double[] real, double[] imag. This could lead to a simpler workaround, but I'd like to know how to do it right.

Lucas Trzesniewski
  • 50,214
  • 11
  • 107
  • 158
Rolf
  • 730
  • 6
  • 14

1 Answers1

5

This is a cornerstone of managed code, a managed compiler is forbidden from making any assumptions about type layout. Only way that code can be verifiable and type-safe across different architectures. And in fact the CLR plays tricks with it, intentionally reordering the members of a type if that produces a better layout.

So a managed Complex structure is not convertible to a comparable NativeComplex, the compiler simply can't assume that these types are in any way identical. Which forces you to copy the array, from an array<Complex> to a NativeComplex[], one element and one member at a time.

Well, this is unpleasant. But you can cheat. It isn't completely unreasonable to do so, native code isn't verifiable anyway. And your structure declaration has a special property, it is a blittable type. Which is an expensive word that means that the CLR does not have a good reason to actually choose a different layout. Whether a struct is in fact blittable is something that is determined at runtime as well, the pinvoke marshaller needs to know. Whose primary job is doing what you are trying to do, calling native code from a managed program and converting function arguments where necessary.

But you are not using the pinvoke marshaller and complex type marshaling like this is not a built-in feature of C++ Interop (aka IJW). You'll have to invoke it yourself:

void Mandelbrot::Computations::Basic(int vectorSize, array<Complex,1>^ points, double maxRadius, int maxLoops, array<int,1>^ result)
{
    pin_ptr<Complex> pPoints = &points[0];
    NativeComplex* pNative = (NativeComplex*)pPoints;    // cheat
    pin_ptr<int> pResult = &result[0];
    NativeComputations::Basic(vectorSize, pNative, maxRadius, maxLoops, pResult);
}

Which isn't pretty but you'll get away with it, if you want fast code then you'll have to. Do keep in mind that this is not in any way an endorsement for blindly casting the pointer in all cases. Surprises do exist, a good example is this question.

Community
  • 1
  • 1
Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
  • Thank you very much Hans. I learned a lot from your answer. But the bottomline for me is: IJW, like P/Invoke, forces me to duplicate declarations of data objects that are to be shared between native and managed. I still need to know the details of struct layout in C++ to do this. If these declarations become complicated or more extensive, much care is necessary. Of course I admit that you need to be careful and know about struct layout details anyway when you're talking C++ :) – Rolf Mar 24 '15 at 09:13