24

I want to pass around 100 - 10,000 Points from an unmanaged C++ to C#.

The C++ side looks like this:

__declspec(dllexport) void detect_targets( char * , int  , /* More arguments */ )
{
    std::vector<double> id_x_y_z;
    // Now what's the best way to pass this vector to C#
}

Now my C# side looks like this:

using System;
using System.Runtime.InteropServices;

class HelloCpp
{

    [DllImport("detector.dll")]

    public static unsafe extern void detect_targets( string fn , /* More arguments */ );

    static void Main()
    {
        detect_targets("test.png" , /* More arguments */ );
    }
}

How do I need to alter my code in order to pass the std::vector from unmanaged C++ with all it's content to C#?

The Vivandiere
  • 3,059
  • 3
  • 28
  • 50
nali
  • 481
  • 1
  • 3
  • 16
  • So you want to return the vector fro the function? – deviantfan Jul 14 '15 at 21:33
  • 1
    How come in C++ your function returns `void` but in C# it returns `int`? And also, it seems like you're trying to pass a vector/array *from* C# *to* C++, not the other way (as your title states). – Jashaszun Jul 14 '15 at 21:35
  • @Jashaszun: Mistake during code simplification – nali Jul 14 '15 at 21:39
  • 1
    @nali Then please fix your question. As it is, there are problems that can confuse potential answerers (such as me). – Jashaszun Jul 14 '15 at 21:39
  • @Jashaszun: Here you go – nali Jul 14 '15 at 21:42
  • @pm100: Which information is missing? The goal is to call a C++ DLL function with arguments detect_targets( ... ) which 'somehow' returns a vector or array or something which stores the detected coordinates. – nali Jul 14 '15 at 21:46
  • As long as the C# side is not changing the length of the vector, can't you just pass it as an array (or `double*`, to avoid the copy)? [`id_x_y_z.data()` or `&id_x_y_z[0]`](http://stackoverflow.com/questions/4289612/getting-array-from-stdvector) – Mitch Jul 14 '15 at 22:24
  • @Mitch: C# does not know the size of the C++ vector. It's dynamic and unpredictable. Maybe I understood you wrong, could you paste some source code examples? – nali Jul 14 '15 at 22:42
  • 1
    @nail, provided below. You can just return the size with the pointer. – Mitch Jul 14 '15 at 23:10
  • @Mitch How do you declare `std::array` parameter without a size or assign to a C-style array which the C++ didn't create though? This is why I'm attempting to use a `std::vector` instead. – Matt Arnold Jan 08 '21 at 11:21
  • @MattArnold, I doubt `std::array` could cross an ABI boundary since it is mostly compile-time sugar for a constant size array. You can still deconstruct a `std::array` to raw pointer + size using `std::array<,>::data()` and `std::array<,>::size()`. (In much the same way as the `std::vector` example below.) Careful to make sure that the pointer remains valid until `ReleaseItems` is called. – Mitch Jan 08 '21 at 18:36

3 Answers3

30

As long as the managed code does not resize the vector, you can access the buffer and pass it as a pointer with vector.data() (for C++0x) or &vector[0]. This results in a zero-copy system.

Example C++ API:

#define EXPORT extern "C" __declspec(dllexport)

typedef intptr_t ItemListHandle;

EXPORT bool GenerateItems(ItemListHandle* hItems, double** itemsFound, int* itemCount)
{
    auto items = new std::vector<double>();
    for (int i = 0; i < 500; i++)
    {
        items->push_back((double)i);
    }

    *hItems = reinterpret_cast<ItemListHandle>(items);
    *itemsFound = items->data();
    *itemCount = items->size();

    return true;
}

EXPORT bool ReleaseItems(ItemListHandle hItems)
{
    auto items = reinterpret_cast<std::vector<double>*>(hItems);
    delete items;

    return true;
}

Caller:

static unsafe void Main()
{
    double* items;
    int itemsCount;
    using (GenerateItemsWrapper(out items, out itemsCount))
    {
        double sum = 0;
        for (int i = 0; i < itemsCount; i++)
        {
            sum += items[i];
        }
        Console.WriteLine("Average is: {0}", sum / itemsCount);
    }

    Console.ReadLine();
}

#region wrapper

[DllImport("Win32Project1", ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)]
static unsafe extern bool GenerateItems(out ItemsSafeHandle itemsHandle,
    out double* items, out int itemCount);

[DllImport("Win32Project1", ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)]
static unsafe extern bool ReleaseItems(IntPtr itemsHandle);

static unsafe ItemsSafeHandle GenerateItemsWrapper(out double* items, out int itemsCount)
{
    ItemsSafeHandle itemsHandle;
    if (!GenerateItems(out itemsHandle, out items, out itemsCount))
    {
        throw new InvalidOperationException();
    }
    return itemsHandle;
}

class ItemsSafeHandle : SafeHandleZeroOrMinusOneIsInvalid
{
    public ItemsSafeHandle()
        : base(true)
    {
    }

    protected override bool ReleaseHandle()
    {
        return ReleaseItems(handle);
    }
}

#endregion
Mitch
  • 21,223
  • 6
  • 63
  • 86
  • Thank you kindly for this solution - just needed it to transfer an entire realtime-generated polygon model from a DLL into an Unity game - but may I ask what the entire system with "ItemsSafeHandle" is required for? I've tested the system completely without the first argument and it did work. Does it add some sort of "safety"? – DragonGamer Mar 04 '17 at 07:52
  • 2
    @DragonGamer, the problem at hand is that your unmanaged code is allocating memory not known to the .Net GC. `SafeHandle` allows the GC to work even in the face of buggy code which would otherwise leak. See [Why `SafeHandle`?](https://msdn.microsoft.com/en-us/library/system.runtime.interopservices.safehandle(v=vs.110).aspx#Remarks) – Mitch Mar 04 '17 at 18:31
14

I implemented this using C++ CLI wrapper. C++ CLI is one the three possible approaches for C++ C# interop. The other two approaches are P/Invoke and COM. (I have seen a few good people recommend using C++ CLI over the other approaches)

In order to marshall information from native code to managed code, you need to first wrap the native code inside a C++ CLI managed class. Create a new project to contain native code and its C++ CLI wrapper. Make sure to enable the /clr compiler switch for this project. Build this project to a dll. In order to use this library, simply add its reference inside C# and make calls against it. You can do this if both projects are in the same solution.

Here are my source files for a simple program to marshal a std::vector<double> from native code into C# managed code.

1) Project EntityLib (C++ CLI dll) (Native Code with Wrapper)

File NativeEntity.h

#pragma once

#include <vector>
class NativeEntity {
private:
    std::vector<double> myVec;
public:
    NativeEntity();
    std::vector<double> GetVec() { return myVec; }
};

File NativeEntity.cpp

#include "stdafx.h"
#include "NativeEntity.h"

NativeEntity::NativeEntity() {
    myVec = { 33.654, 44.654, 55.654 , 121.54, 1234.453}; // Populate vector your way
}

File ManagedEntity.h (Wrapper Class)

#pragma once

#include "NativeEntity.h"
#include <vector>
namespace EntityLibrary {
    using namespace System;

    public ref class ManagedEntity {
    public:
        ManagedEntity();
        ~ManagedEntity();

        array<double> ^GetVec();
    private:
        NativeEntity* nativeObj; // Our native object is thus being wrapped
    };

}

File ManagedEntity.cpp

#include "stdafx.h"
#include "ManagedEntity.h"

using namespace EntityLibrary;
using namespace System;


ManagedEntity::ManagedEntity() {
    nativeObj = new NativeEntity();
}

ManagedEntity::~ManagedEntity() {
    delete nativeObj;

}

array<double>^ ManagedEntity::GetVec()
{
    std::vector<double> tempVec = nativeObj->GetVec();
    const int SIZE = tempVec.size();
    array<double> ^tempArr = gcnew array<double> (SIZE);
    for (int i = 0; i < SIZE; i++)
    {
        tempArr[i] = tempVec[i];
    }
    return tempArr;
}

2) Project SimpleClient (C# exe)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using EntityLibrary;

namespace SimpleClient {

    class Program {
        static void Main(string[] args) {
            var entity = new ManagedEntity();
            for (int i = 0; i < entity.GetVec().Length; i++ )
                Console.WriteLine(entity.GetVec()[i]);
        }
    }
}
The Vivandiere
  • 3,059
  • 3
  • 28
  • 50
  • 2
    Hey may I know what the "^" operator means when you place it at the array^ Managed::Entity::GetVec() function ? and also at the array ^GetVec() – calveeen Jun 02 '19 at 09:39
  • @calveeen https://stackoverflow.com/questions/202463/what-does-the-caret-mean-in-c-cli – Cole Tobin Jan 18 '20 at 14:53
8

I could think of more than one option, but all include copying the data of the array anyways. With [out] parameters you could try:

C++ code

__declspec(dllexport) void __stdcall detect_targets(wchar_t * fn, double **data, long* len)
{
    std::vector<double> id_x_y_z = { 1, 2, 3 };

    *len = id_x_y_z.size();
    auto size = (*len)*sizeof(double);

    *data = static_cast<double*>(CoTaskMemAlloc(size));
    memcpy(*data, id_x_y_z.data(), size);
}

C# code

[DllImport("detector.dll")]
public static extern void detect_targets(
    string fn, 
    [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 2)] out double[] points, 
    out int count);

static void Main()
{
    int len;
    double[] points;

    detect_targets("test.png", out points, out len);
}
Nikolay
  • 10,752
  • 2
  • 23
  • 51
  • 2
    +1, but I would reiterate that the returned array [must be allocated with `CoTaskMemAlloc`](https://msdn.microsoft.com/en-us/library/z6cfh6e6(v=vs.110).aspx) to avoid corruption or leaks. – Mitch Jul 14 '15 at 23:33
  • @Mitch Thx. Also, I might be making an unnecessary deep copy. – The Vivandiere Apr 20 '16 at 12:27
  • 2
    sometimes my count returns as a different number than the array.. I am not using doubles, but a custom struct. Where does the value 2 come from? – user1000247 Aug 03 '17 at 15:04
  • @user1000247 if I'm looking at the documentation correctly, it's the zero based index of the array size. So in this case, count is the third parameter, and therefore index 2. https://msdn.microsoft.com/en-us/library/system.runtime.interopservices.marshalasattribute.sizeparamindex%28v=vs.110%29.aspx – Andy Mar 28 '18 at 10:18