Question

Let me describe my problem - I have a struct that wraps an unmanaged handle (let's call it Mem). I need this handle to call a particular method (say "retain" or alternatively, maintain a reference count) whenever it is copied.

In other words, I need a struct that maintains a reference count internally (I have a mechanism externally as well, but need a way to invoke that mechanism).

Unfortunately, C# doesn't let me do this in any way.

I also cannot make Mem a class because I will pass an array of these structs to unmanaged code and I do NOT want to convert them one by one before passing them in (just pin and pass).

Does anyone know of any workaround (IL Weaving, etc) that can be applied to add this behavior in? I believe IL doesn't prevent me from doing this, only C#, correct?

I am happy to answer any questions about the framework and restrictions I have, but I am not looking for - "please change your design" or "don't use C# for this" answers, thanks very much.

Was it helpful?

Solution 3

Edit: I hosted the work from this answer on GitHub: the NOpenCL library.

Based on your comment, I determined the following as an appropriate long-term course of action for the problems being discussed here. Apparently the problem centers around the use of OpenCL within managed code. What you need is a proper interop layer for this API.

As an experiment, I wrote a managed wrapper for a large portion of the OpenCL API to evaluate the viability of SafeHandle to wrap cl_mem, cl_event, and other objects which require a call to clRelease* for cleanup. The most challenging part was implementing methods like clEnqueueReadBuffer which can take an array of these handles as a parameter. The initial declaration of this method looked like the following.

[DllImport(ExternDll.OpenCL)]
private static extern ErrorCode clEnqueueReadBuffer(
    CommandQueueSafeHandle commandQueue,
    BufferSafeHandle buffer,
    [MarshalAs(UnmanagedType.Bool)] bool blockingRead,
    IntPtr offset,
    IntPtr size,
    IntPtr destination,
    uint numEventsInWaitList,
    [In, MarshalAs(UnmanagedType.LPArray)] EventSafeHandle[] eventWaitList,
    out EventSafeHandle @event);

Unfortunately, the P/Invoke layer does not support marshaling an array of SafeHandle objects, so I implemented an ICustomMarshaler called SafeHandleArrayMarshaler to handle this. Note that the current implementation does not use Constrained Execution Regions, so an asynchronous exception during marshaling can cause it to leak memory.

internal sealed class SafeHandleArrayMarshaler : ICustomMarshaler
{
    private static readonly SafeHandleArrayMarshaler Instance = new SafeHandleArrayMarshaler();

    private SafeHandleArrayMarshaler()
    {
    }

    public static ICustomMarshaler GetInstance(string cookie)
    {
        return Instance;
    }

    public void CleanUpManagedData(object ManagedObj)
    {
        throw new NotSupportedException();
    }

    public void CleanUpNativeData(IntPtr pNativeData)
    {
        if (pNativeData == IntPtr.Zero)
            return;

        GCHandle managedHandle = GCHandle.FromIntPtr(Marshal.ReadIntPtr(pNativeData, -IntPtr.Size));
        SafeHandle[] array = (SafeHandle[])managedHandle.Target;
        managedHandle.Free();

        for (int i = 0; i < array.Length; i++)
        {
            SafeHandle current = array[i];
            if (current == null)
                continue;

            if (Marshal.ReadIntPtr(pNativeData, i * IntPtr.Size) != IntPtr.Zero)
                array[i].DangerousRelease();
        }

        Marshal.FreeHGlobal(pNativeData - IntPtr.Size);
    }

    public int GetNativeDataSize()
    {
        return IntPtr.Size;
    }

    public IntPtr MarshalManagedToNative(object ManagedObj)
    {
        if (ManagedObj == null)
            return IntPtr.Zero;

        SafeHandle[] array = (SafeHandle[])ManagedObj;
        int i = 0;
        bool success = false;
        try
        {
            for (i = 0; i < array.Length; success = false, i++)
            {
                SafeHandle current = array[i];
                if (current != null && !current.IsClosed && !current.IsInvalid)
                    current.DangerousAddRef(ref success);
            }

            IntPtr result = Marshal.AllocHGlobal(array.Length * IntPtr.Size);
            Marshal.WriteIntPtr(result, 0, GCHandle.ToIntPtr(GCHandle.Alloc(array, GCHandleType.Normal)));
            for (int j = 0; j < array.Length; j++)
            {
                SafeHandle current = array[j];
                if (current == null || current.IsClosed || current.IsInvalid)
                {
                    // the memory for this element was initialized to null by AllocHGlobal
                    continue;
                }

                Marshal.WriteIntPtr(result, (j + 1) * IntPtr.Size, current.DangerousGetHandle());
            }

            return result + IntPtr.Size;
        }
        catch
        {
            int total = success ? i + 1 : i;
            for (int j = 0; j < total; j++)
            {
                SafeHandle current = array[j];
                if (current != null)
                    current.DangerousRelease();
            }

            throw;
        }
    }

    public object MarshalNativeToManaged(IntPtr pNativeData)
    {
        throw new NotSupportedException();
    }
}

This allowed me to successfully use the following interop declaration.

[DllImport(ExternDll.OpenCL)]
private static extern ErrorCode clEnqueueReadBuffer(
    CommandQueueSafeHandle commandQueue,
    BufferSafeHandle buffer,
    [MarshalAs(UnmanagedType.Bool)] bool blockingRead,
    IntPtr offset,
    IntPtr size,
    IntPtr destination,
    uint numEventsInWaitList,
    [In, MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(SafeHandleArrayMarshaler))] EventSafeHandle[] eventWaitList,
    out EventSafeHandle @event);

This method is declared as private so I could expose it through a method that handles the numEventsInWaitList and eventWaitList arguments properly according to the OpenCL 1.2 API documentation.

internal static EventSafeHandle EnqueueReadBuffer(CommandQueueSafeHandle commandQueue, BufferSafeHandle buffer, bool blocking, IntPtr offset, IntPtr size, IntPtr destination, EventSafeHandle[] eventWaitList)
{
    if (commandQueue == null)
        throw new ArgumentNullException("commandQueue");
    if (buffer == null)
        throw new ArgumentNullException("buffer");
    if (destination == IntPtr.Zero)
        throw new ArgumentNullException("destination");

    EventSafeHandle result;
    ErrorHandler.ThrowOnFailure(clEnqueueReadBuffer(commandQueue, buffer, blocking, offset, size, destination, eventWaitList != null ? (uint)eventWaitList.Length : 0, eventWaitList != null && eventWaitList.Length > 0 ? eventWaitList : null, out result));
    return result;
}

The API is finally exposed to user code as the following instance method in my ContextQueue class.

public Event EnqueueReadBuffer(Buffer buffer, bool blocking, long offset, long size, IntPtr destination, params Event[] eventWaitList)
{
    EventSafeHandle[] eventHandles = null;
    if (eventWaitList != null)
        eventHandles = Array.ConvertAll(eventWaitList, @event => @event.Handle);

    EventSafeHandle handle = UnsafeNativeMethods.EnqueueReadBuffer(this.Handle, buffer.Handle, blocking, (IntPtr)offset, (IntPtr)size, destination, eventHandles);
    return new Event(handle);
}

OTHER TIPS

I believe IL doesn't prevent me from doing this, only C#, correct?

Yes, that's right where "this" is "a parameterless constructor for a struct". I blogged about that a while ago.

However, having a parameterless constructor does not do what you want in terms of notifying you every time a struct is copied. There's basically no way of doing that, as far as I'm aware. The constructor isn't even called in every case when you end up with a "default" value, and even if it were, it's certainly not called just for copy operations.

I know you don't want to hear "please change your design" but you're simply asking for something which does not exist in .NET.

I would suggest having some sort of method on the value type which returns a new copy, having taken appropriate action. You then need to make sure you always call that method at the right time. There will be nothing preventing you from getting this wrong, other than whatever testing you can build.

Does anyone know of any workaround (IL Weaving, etc) that can be applied to add this behavior in? I believe IL doesn't prevent me from doing this, only C#, correct?

This is correct, somewhat. The reason C# prevents this is that the constructor will not be used, even if its defined in IL, in many cases. Your case is one of these - if you create an array of structs, the constructors will not be called, even if they're defined in the IL.

Unfortunately, there isn't really a workaround since the CLR won't call the constructors, even if they exist.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top