3

I have a matrix class which contains a fixed size float array that I want to expose directly in the C# wrapper class:

class Matrix
{
public:
    float data[16];
    ...
};

Normally, when wrapping this structure in C# I would define the wrapped type as a POD struct that would utilize [StructLayout(LayoutKind.Sequential)] and would allow me to directly marshall the class to and from C++ / C#. However, the c++ class exposes other types (via members or functions) which can't be marshalled in this way so I instead have to use a PIMPL mechanism where my C# class contains an opaque pointer (IntPtr) to the underlying C++ structure. However, I would still like to directly expose the underlying data member of the c++ structure to the C# object. By directly, I mean that modifications to the elements of data in C# will be directly mirrored in the underlying C++ structure (i.e. the C# member can not be a copy of the C++ member). My idea was to expose the data member in C# like this:

public class Matrix
{
    protected HandleRef cptr; // opaque reference to equivalent C++ instance
    public float [] data
    {
        get
        {
            return Get_data(cptr);
        }
    }

    [DllImport(MY_CPP_DLL, EntryPoint = "CSharp_Get_data", CharSet = CHARSET)]
    [return: MarshalAs(UnmanagedType.LPArray, SizeConst = 16, ArraySubType = UnmanagedType.R4)]
    private static extern float [] Get_Data(HandleRef in_object);
};

Where my C++ dll contains the function CSharp_Get_data which looks like this:

DLLEXPORT float * STDCALL CSharp_Get_data(Matrix *obj)
{
    return obj->data;
}

This compiles fine in both C++ and C#, but when I try and do something like this in c#:

Matrix asdf = new Matrix();
asdf.data[0] = 2.0f;

I receive a System.Runtime.InteropServices.MarshalDirectiveException which states Additional information: Cannot marshal 'return value': Invalid managed/unmanaged type combination.

Is there a way I can use this approach but not encounter this error? I know this is a pretty odd use case but I feel as if the approach does not violate any of the marshalling standards described by MS here: https://learn.microsoft.com/en-us/dotnet/framework/interop/default-marshaling-for-arrays.

Thanks for any help.

9
  • This code is almost impossible to use correctly in a C++ program, it doesn't get better when you pinvoke it. Arrays are very primitive, you can get a pointer to it but you don't know how many elements it contains. SizeParamIndex = 0 does not make sense, parameter 0 does not return the size. The pinvoke marshaller choked on that one. Fixing it will cause a crash when it tries to release the memory for the array. Consider int CSharp_Get_data(const Matrix* obj, float* data, size_t maxdata), now the C++ code simply fills the array you pass. Commented Sep 19, 2018 at 22:10
  • learn.microsoft.com/en-us/dotnet/framework/interop/… Commented Sep 19, 2018 at 22:14
  • The OP linked the same URL in his question explaining that it doesn't apply to his use case... Commented Sep 19, 2018 at 22:25
  • @HansPassant It does seem I was using SizeParamIndex in an incorrect manner so I'll remove that from the original post. However, the alternative you suggested still won't work as underlying memory for the data member in C# will be different than the C++ object (i.e. still a copy, so if a user modifies data[0] in the C# object it will not be reflected in the C++ object's data[0] element). Commented Sep 19, 2018 at 22:33
  • Sure, the C++ array is not anything like a C# array, so copying is required. If you want to live dangerously then that is easy to do, simply declare the return type as float* in the pinvoke declaration. The unsafe keyword is required, it is. Commented Sep 19, 2018 at 22:38

1 Answer 1

2

You can't get a float[] unless .NET allocates the memory. That's true of StructLayout(Sequential), too -- fixed-size buffers such as float[16] are not compatible with float[].

What you can do, though, if you want convenience and efficiency more than you want float[], is to use the new zero-copy buffering types. You can construct a System.Span<float> from a pointer to the first element plus the number of elements, and subscripting that span will give you direct access to the memory. Some refactoring will need to be done, but since Span<T> can access .NET arrays equally well, once you rewrite your code to use Span, it can live in both worlds (C# and C++-allocated data).

Sign up to request clarification or add additional context in comments.

1 Comment

Very interesting, this seems exactly like what I need! Sadly, I can't use C# 7+ but if no one comes a long and posts a more generic solution I will mark this answer as accepted.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.