3

no matter what I try. I appear to get garbage results when I marshal the data across! The data after the marshal copy just contains an array of what looks like uninitialized data. Just pure garbage.

Thanks for your help in advance!

C++

typedef uint64_t TDOHandle;

extern "C" DATAACCESSLAYERDLL_API const TDOHandle * __stdcall DB_GetRecords()
{
    const Database::TDBRecordVector vec = Database::g_Database.GetRecords();
    if (vec.size() > 0)
    {
        return &vec[0];
    }
    return nullptr;
}

C#

The declaration

    [System.Security.SuppressUnmanagedCodeSecurity()]
    [DllImport("DataCore.dll")]
    static private extern IntPtr DB_GetRecords();

//The marshalling process

            IntPtr ptr_records = DB_GetRecords();
        if (ptr_records != null)
        {
            Byte[] recordHandles = new Byte[DB_GetRecordCount()*sizeof (UInt64)];
            Marshal.Copy(ptr_records, recordHandles, 0, recordHandles.Length);

            Int64[] int64Array = new Int64[DB_GetRecordCount()];
            Buffer.BlockCopy(recordHandles, 0, int64Array, 0, recordHandles.Length);
        }
Asheh
  • 1,547
  • 16
  • 25
  • You can write directly to the `int64Array` with `Marshal.Copy`. – xanatos Mar 26 '15 at 14:47
  • There is no overload for UInt64 :( – Asheh Mar 26 '15 at 14:51
  • You can cast a `long[]` to a `ulong[]` if you go through a `object` cast... see http://stackoverflow.com/questions/593730/why-does-int-is-uint-true-in-c-sharp So, `ulong[] uint64Array = new ulong[...]`; `Marshal.Copy(ptr_records, (long[])(object)uint64Array, 0, uint64Array.Length)` – xanatos Mar 26 '15 at 14:55
  • you can also return a [`SAFEARRAY`](https://msdn.microsoft.com/en-us/library/windows/desktop/ms221482(v=vs.85).aspx) and the [runtime is smart enough to pick up on that for you](https://msdn.microsoft.com/en-us/library/vstudio/z6cfh6e6(v=vs.100).aspx) – Mgetz Mar 26 '15 at 15:47

2 Answers2

4

You are returning the address of memory owned by a local variable. When the function returns, the local variable is destroyed. Hence the address you returned is now meaningless.

You need to allocate dynamic memory and return that. For instance, allocate it with CoTaskMemAlloc. Then the consuming C# can deallocate it with a call to Marshal.FreeCoTaskMem.

Or allocate the memory using new, but also export a function from your unamanaged code that can deallocate the memory.

For example:

if (vec.size() > 0)
{
    TDOHandle* records = new TDOHandle[vec.size()];
    // code to copy content of vec to records
    return records;
}
return nullptr;

And then you would export another function that exposed the deallocator:

extern "C" DATAACCESSLAYERDLL_API void __stdcall DB_DeleteRecords(
    const TDOHandle * records)
{
    if (records)
        delete[] record;
}

All that said, it seems that you can obtain the array length before you call the function to populate the array. You do that with DB_GetRecordCount(). In that case you should create an array in your managed code, and pass that to the unmanaged code for it to populate. That side steps all the issues of memory management.

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • Ah crap you have spotted it. Well done! The function actually returns a reference to the data, which I was supposed to return. Not make a copy... – Asheh Mar 26 '15 at 14:51
  • @Asheh If you need to have a `DB_GetRecordCount()` then it's probably better/easier to pass two parameters to DB_GetRecords: a an array of ulong that the caller has to create and the length of that array. In this way the array is created C#-side, filled C++ side and you don't have to copy back the memory. I do know that I'm explaining it in a terrible way :-) – xanatos Mar 26 '15 at 15:00
  • @xanatos Hmm. I had not spotted `DB_GetRecordCount`. But yes, if you have that then you are right. – David Heffernan Mar 26 '15 at 15:03
1

I'll add that there is another way to do it:

public sealed class ULongArrayWithAllocator
{
    // Not necessary, default
    [UnmanagedFunctionPointer(CallingConvention.StdCall)]
    public delegate IntPtr AllocatorDelegate(IntPtr size);

    private GCHandle Handle;

    private ulong[] allocated { get; set; }

    public ulong[] Allocated
    {
        get
        {
            // We free the handle the first time the property is
            // accessed (we are already C#-side when it is accessed)
            if (Handle.IsAllocated)
            {
                Handle.Free();
            }

            return allocated;
        }
    }

    // We could/should implement a full IDisposable interface, but
    // the point of this class is that you use it when you want
    // to let C++ allocate some memory and you want to retrieve it,
    // so you'll access LastAllocated and free the handle
    ~ULongArrayWithAllocator()
    {
        if (Handle.IsAllocated)
        {
            Handle.Free();
        }
    }

    // I'm using IntPtr for size because normally 
    // sizeof(IntPtr) == sizeof(size_t) and vector<>.size() 
    // returns a size_t
    public IntPtr Allocate(IntPtr size)
    {
        if (allocated != null)
        {
            throw new NotSupportedException();
        }

        allocated = new ulong[(long)size];
        Handle = GCHandle.Alloc(allocated, GCHandleType.Pinned);
        return Handle.AddrOfPinnedObject();
    }
}

[DllImport("DataCore.dll", CallingConvention = CallingConvention.StdCall)]
static private extern IntPtr DB_GetRecords(ULongArrayWithAllocator.AllocatorDelegate allocator);

and to use it:

var allocator = new ULongArrayWithAllocator();
DB_GetRecords(allocator.Allocate);

// Here the Handle is freed
ulong[] allocated = allocator.Allocated; 

and C++ side

extern "C" DATAACCESSLAYERDLL_API void __stdcall DB_GetRecords(TDOHandle* (__stdcall *allocator)(size_t)) {
    ...
    // This is a ulong[vec.size()] array, that you can
    // fill C++-side and can retrieve C#-side
    TDOHandle* records = (*allocator)(vec.size());
    ...
}

or something similar :-) You pass a delegate to the C++ function that can allocate memory C#-side :-) And then C# side you can retrieve the last memory that was allocated. It is important that you don't make more than one allocation C++-side in this way in a single call, because you are saving a single LastAllocated reference, that is "protecting" the allocated memory from the GC (so don't do (*allocator)(vec.size());(*allocator)(vec.size());)

Note that it took me 1 hour to write correctly the calling conventions of the function pointers, so this isn't for the faint of heart :-)

xanatos
  • 109,618
  • 12
  • 197
  • 280