3

I need to convert a double array in c# to an IntPtr to properly send it to my c DLL. I have successfully been able to convert from IntPtr to double[,] using the method from this answer. Ive also found another answer that is close to what I need but deals with 1D arrays.

Im missing something in my logic and get the error code after a crash: Managed ' has exited with code -1073740940 (0xc0000374).

This is what I have so far

            IntPtr p = Marshal.AllocHGlobal(rows * columns);
            for (int i = 0; i < rows; i++) //rows
            {
                double[] temp = new double[columns];

                for (int x = 0; x < columns; x++)
                    temp[x] = input.cells[i, x];

                Marshal.Copy(temp, 0, p, columns);

                p = (IntPtr)(p.ToInt64() + IntPtr.Size);
            }

            toReturn.cells = p;
            Marshal.FreeHGlobal(p);
            return toReturn;

toReturn.cells is my IntPtr inside a struct that I return. cells is structured as cells[rows, columns]

IntPtr's are still very new to me.

Edit: thanks to harold. their suggestions worked beautifully

  • 1
    Do you have a `double[][]` at all? The code appears to be using a `double [,] input` – Ben Voigt Mar 11 '21 at 15:51
  • @BenVoigt yes you're right. Im currently looking at the comments and answers to see if I can fix the problem. Thank you for your comments! – 6seven8nine Mar 11 '21 at 16:04

2 Answers2

5

There are various things wrong with that.

First, rows * columns is not the size of the data, it's only the total number of elements. The elements are not one byte each, but eight, or sizeof(double) if you prefer.

Second, p is updated by p = (IntPtr)(p.ToInt64() + IntPtr.Size); (ie advancing it by 4 or 8 bytes depending on how big pointers are in the current mode), but you've written columns * 8 (or, columns * sizeof(double)) bytes of data. Advancing p by less than columns * 8 makes the writes overwrite each other, so not all data ends up in the result. By the way, the complicated conversions here are actually not necessary, you can add directly to an IntPtr, since .NET 4.

Third, p is changed in the loop, which is not bad on its own, but it's done in a way that loses track of the original pointer. toReturn.cells = p; and Marshal.FreeHGlobal(p); use a p which does not refer to the area that you allocated, they use a p which now points just past the end of the data (well it would point there if p was updated by the right amount). The original p must be remembered.

Fourth, freeing the data before returning means that it now no longer exists, so whatever code this data is passed to, still doesn't have it: it has a pointer to nothing which will be invalid to use (it may accidentally work, but it's dangerous and wrong).

The first three points are easy to fix, but the last one needs a non-local change to how your application works: the memory cannot be freed here, but it should be freed at some point, namely when the user of the data is done with it.

Some fixes applied:

        int stride = columns * sizeof(double);
        IntPtr p = Marshal.AllocHGlobal(rows * stride);
        for (int i = 0; i < rows; i++) //rows
        {
            double[] temp = new double[columns];

            for (int x = 0; x < columns; x++)
                temp[x] = input.cells[i, x];

            Marshal.Copy(temp, 0, p + stride * i, columns);
        }

        toReturn.cells = p;
        return toReturn;

Keep in mind you should still free the memory at the appropriate time.

harold
  • 61,398
  • 6
  • 86
  • 164
  • Thank you very much. This was so helpful in many ways. Along with your suggestions, I allocated the IntPtr outside of the function and passed it in, so I could free the memory when I was done with it. I also was mistaken when I said my cells were formated as cells[rows,columns]. It was the other way around. Have a good one! – 6seven8nine Mar 11 '21 at 16:29
  • You can extract a row of a 2D array in `C#` using `Buffer.Copy()` when you get it right it is fast and eliminates the inner loop above. In fact you probably do not need `temp` and you might do it all in `Marshal.Copy()` (maybe). – John Alexiou Mar 11 '21 at 17:40
  • @JohnAlexiou there is no overload of `Marshal.Copy` that takes a 2D array, but I agree there are various options to improve this – harold Mar 11 '21 at 17:47
  • @harold - [here is a fiddle](https://dotnetfiddle.net/TEBONe) that shows how to extract a 1D row from a 2D array using `Buffer.BlockCopy()`. – John Alexiou Mar 11 '21 at 18:00
2

What about using GHandles to get the IntPtr to an array. I made a copy first to avoid overwriting data by the client. Unfortunately, you need to keep a GHandle in order to call .Free() to avoid memory leaks. This drives the decision to keep not only an IntPtr in the outputs, but also the GHandle and the byte length FYI.

static class Program
{
    static void Main(string[] args)
    {
        Inputs inputs = new Inputs();
        inputs.Cells = new double[,] { { 1, 2, 3 }, { 4, 5, 6 }, { 7, 8, 9 } };
        
        var outputs = Prepare(inputs);
        Console.WriteLine(outputs.CellPtr);            

        Console.WriteLine("Finish.");
    }

    static Outputs Prepare(Inputs inputs)
    {
        Outputs outputs = new Outputs();
        outputs.ByteSize = Buffer.ByteLength(inputs.Cells);
        var temp = new double[inputs.Cells.GetLength(0), inputs.Cells.GetLength(1)];
        Buffer.BlockCopy(inputs.Cells, 0, temp, 0, outputs.ByteSize);
        outputs.Handle = GCHandle.Alloc(inputs.Cells, GCHandleType.Pinned);
        outputs.CellPtr = outputs.Handle.AddrOfPinnedObject();
        return outputs;
    }

}
public class Inputs
{
    public double[,] Cells { get; set; }
}

public class Outputs : IDisposable
{
    public IntPtr CellPtr { get; set; }
    public int ByteSize { get; set; }
    public GCHandle Handle { get; set; }

    #region IDisposable Support
    private bool disposedValue = false; // To detect redundant calls

    protected virtual void Dispose(bool disposing)
    {
        if (!disposedValue)
        {
            if (disposing)
            {
                // dispose managed resources here
            }

            Handle.Free();
            CellPtr = IntPtr.Zero;
            ByteSize = 0;

            disposedValue = true;
        }
    }

    ~Outputs()
    {
        // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
        Dispose(false);
    }

    // This code added to correctly implement the disposable pattern.
    public void Dispose()
    {
        Dispose(true);
    }
    #endregion
}
John Alexiou
  • 28,472
  • 11
  • 77
  • 133