lukecampbell
lukecampbell

Reputation: 15256

Safe and Correct Struct Marshalling

Unmanaged and Managed Memory Regions

I am attempting to execute unmanaged code from a C-library. One of the methods takes a void* as a parameter but under the hood it's cast to a struct of type nc_vlen_t

C struct for nc_vlen_t

/** This is the type of arrays of vlens. */
typedef struct {
    size_t len; /**< Length of VL data (in base type units) */
    void *p;    /**< Pointer to VL data */
} nc_vlen_t;

Executing the method is correct and it works, I am concerned more about the pinning and safe handling of managed and unmanaged memory regions. I want to be as certain as possible that I am not going to cause memory leaks or a SEGFAULT. I wrote a struct that will be marshalled to and from the nc_vlen_t when I execute the C-library method calls.

C# struct

[StructLayout(LayoutKind.Sequential)]
public struct VlenStruct {
    public Int32 len;
    public IntPtr p; // Data 
}

The struct consists of a size_t that indicates the array length and a void * to the data. Inside the library it has attributes that allow it to cast the (void*) to the appropriate numeric types and I've had great success with that so far.

What I want to understand is the best way to handle the memory regions. After reading some articles and other SO questions this is my best guess for how to handle it. I have a class that acts as an arbiter for creating and managing the structs and their memory. I rely on a destructor to free the handle which will unpin the array so that the GC can do it's job.

C# Vlen Helper

public class Vlen {
    private GCHandle handle;
    private VlenStruct vlen_t;

    public Vlen() {
        isNull = true;
    }

    public Vlen(Array t) {
        isNull = false;
        handle = GCHandle.Alloc(t, GCHandleType.Pinned); // Pin the array
        vlen_t.len = t.Length;
        vlen_t.p = Marshal.UnsafeAddrOfPinnedArrayElement(t, 0); // Get the pointer for &t[0]
    }
    ~Vlen() {
        if(!isNull) {
            handle.Free(); // Unpin the array
        }
    }

    public VlenStruct ToStruct() {
        VlenStruct retval = new VlenStruct();
        retval.len = vlen_t.len;
        retval.p = vlen_t.p;
        return retval;
    }

    private bool isNull;
}

C Method Declaration

    //int cmethod(const int* typep, void *data)
    //  cmethod copies the array contents of the vlen struct to a file
    //  returns 0 after successful write
    //  returns -1 on fail
    [DllImport("somelib.dll", CharSet = CharSet.Ansi, SetLastError = true, ExactSpelling = true, CallingConvention=CallingConvention.Cdecl)]
    public static extern Int32 cmethod(ref Int32 typep, ref VlenStruct data);

If I use this class to create the struct is there a possibility that the GC will clean the array before the C-library is called in this situation:

C# Use-case

{ 
    double[] buffer vlenBuffer = new double[] { 0, 12, 4};
    Vlen data = new Vlen(vlenBuffer); // The instance now pins buffer
    VlenStruct s = data.ToStruct()
    Int32 type = VLEN_TYPE;  
    cmethod(ref type, ref s);
}

Is it possible for the data instance to be cleaned and thereby unpin buffer which could cause unpredictable behavior when executing the external library method?

Upvotes: 2

Views: 1860

Answers (1)

Hans Passant
Hans Passant

Reputation: 941317

Yes, you certainly have a problem here. As far as the jitter is concerned, the lifetime of your "data" object ends just before the ToStruct() method returns. Check this answer for the reason why. Which permits the finalizer to run while your unmanaged code is running. Which unpins your array. It would actually take another garbage collection to corrupt the data that the unmanaged code uses. Very rare indeed but not impossible. You are not likely to get an exception either, just random data corruption.

One workaround is to extend the lifetime of the Vlen object beyond the call, like this:

Vlen data = ...
...
cmethod(ref type, ref s);
GC.KeepAlive(data);

Which works but doesn't win any prizes, easy to forget. I would do this differently:

public static void CallHelper<T>(int type, T[] data) {
    var hdl = GCHandle.Alloc(data, GCHandleType.Pinned);
    try {
        var vlen = new nc_vlen();
        vlen.len = data.Length;
        vlen.data = hdl.AddrOfPinnedObject();
        cmethod(ref type, ref vlen);
    }
    finally {
        hdl.Free();
    }
}

Usage:

var arr = new int[] { 1, 2, 3 };
CallHelper(42, arr);

Which, beyond avoiding the early collection problem, also keeps the array pinned as short as possible. Do note that ref on the first argument of this function is pretty strange, you would not expect this function to alter the data type.

Upvotes: 3

Related Questions