sharpdx / SharpDX

SharpDX GitHub Repository
http://sharpdx.org
MIT License
1.7k stars 638 forks source link

Expose internal methods / Low-level wrapper? #722

Open OndrejPetrzilka opened 8 years ago

OndrejPetrzilka commented 8 years ago

I've encountered a situation when it would be convenient to have access to internal methods several times. These methods are mostly the ones taking IntPtr as arguments.

For example: void SetUnorderedAccessViews(int startSlot, int numUAVs, IntPtr unorderedAccessViewsOut, IntPtr uAVInitialCountsRef)

This would allow me to write extension methods (for convenience) and in several cases avoid filling array of values which gets eventually copied to stackallocated array and passed to DX.

Temporary arrays are annoying, especially because I have to store them somewhere (I don't want to allocate new arrays everywhere).

I'd like to discuss the topic. I know it would be probably a lot of work, but what do you think about creating Low-level DirectX wrapper? It would be separate classes with static methods only (pass instance pointer as first argument), all arguments would be compatible with native API (primitive types, Vectors, IntPtr).

Then classic SharpDX wrapper would be built on the top of that.

There are cases when this would be very convenient, for example: DirectWrite TextLayout I draw every frame different text, but I don't want to allocate new TextLayout object every time just to draw it once and let GC collect it. In this case I would use low-level API.

xoofx commented 8 years ago

This is something I have been thinking recently, to move SharpDX to a struct based approach instead of using classes for ComObjects. It requires to change the code gen. The problem is that lots of APIs are huge to transform to this model (like Direct2D, DirectWrite). I have currently no much spare time for heavy code changes for SharpDX, but if you want to tackle this, I can give more details

OndrejPetrzilka commented 8 years ago

It seems that struct inheritance would be very useful for this case, unfortunately it's not available. What do you have in mind, something like code below? (that's my PhysX wrapper)

 // Original inheritance PxRigidBody:PxRigidActor:PxActor:PxBase

public struct PxRigidBody : INativeInterface
{
    internal physx::PxRigidBody* Native; // Could be IntPtr

// No struct inheritance, thus conversion operators
    public static explicit operator PxRigidBody(PxActor obj);
    public static explicit operator PxRigidBody(PxBase obj);
    public static explicit operator PxRigidBody(PxRigidActor obj);
    public static implicit operator PxBase(PxRigidBody obj);
    public static implicit operator PxActor(PxRigidBody obj);
    public static implicit operator PxRigidActor(PxRigidBody obj);

// Interface implementation
    public bool IsNull { get; }
    public IntPtr Pointer { get; }
    public IntPtr UserData { get; set; }
    public void Release();

// PxRigidBody methods
    public void AddForce(ref Vector3 force, PxForceMode mode, bool autowake);
    public void AddTorque(ref Vector3 torque, PxForceMode mode, bool autowake);
    public void ClearForce();
// ...more PxRigidBody methods

// PxRigidActor methods
    public void AttachShape(PxShape shape);
    public PxShape CreateShape(PxGeometryPtr geometry, PxMaterial material);
    public void DetachShape(PxShape shape);
// ...more PxRigidActor methods

// PxActor methods
    public PxActorFlag GetActorFlags();
    public PxAggregate GetAggregate();
    public sbyte GetName();
// ...etc....
}

Few notes: 1) Struct represents native pointer to object and it's used in very similar fashion 2) It contains all conversion operators for base types (upcasting explicit, downcasting implicit) 3) It contains all the method from all base classes (otherwise using it would require constant casting) 4) You're responsible to call Release()

xoofx commented 8 years ago

Use struct, interface and extension methods with generics. Something like this:

public struct ComObject : ComObject.Interface
{
    public IntPtr NativePointer { get; set; }

    public interface Interface
    {
        IntPtr NativePointer { get; set; }
    }
}

public static class ComObjectExtensions
{
    public static int AddReference<T>(this T comObject) where T : ComObject.Interface
    {
        return Marshal.AddRef(comObject.NativePointer);
    }

    public static int ReleaseReference<T>(this T comObject) where T : ComObject.Interface
    {
        return Marshal.Release(comObject.NativePointer);
    }

    public static int QueryInterface<T>(this T comObject, ref Guid guid, out IntPtr interfacePtr) where T : ComObject.Interface
    {
        return Marshal.QueryInterface(comObject.NativePointer, ref guid, out interfacePtr);
    }
}

public struct Device : Device.Interface
{
    public IntPtr NativePointer { get; set; }

    public interface Interface : ComObject.Interface
    {
    }
}
xoofx commented 8 years ago

The idea is to move interop code into extension methods. The good thing about this is that all ComObject structs contain only a pointer to the com object, so they are acting exactly like a reference. The downside of this approach is that generic code will generate JIT/AOT for each struct that use a " base" method. For instance, in the example above, if you do a device.Release() it will generate a code specifically for the struct Device even though the code is the same for all ComObject.Interface.

It is not so much a problem for DirectX, because apart for the IUnknown interface, not so many objects are using base methods, but it is not ideal.

OndrejPetrzilka commented 8 years ago

I see, in my case above, there was also different Release() for every type in hierarchy (truly different, not just different generic instantiation)

I'm not sure about VS2015, but in VS2013, there's error: Circular base class dependency involving 'ComObject' and 'ComObject.Interface' I had to put interfaces out of the class which implements them.

IL seems fine:

IL_0000: nop
IL_0001: ldarga.s comObject
IL_0003: constrained. !!T
IL_0009: callvirt instance native int ConsoleApplication5.ComObjectInterface::get_NativePointer()
IL_000e: call int32 [mscorlib]System.Runtime.InteropServices.Marshal::AddRef(native int)
IL_0013: stloc.0
IL_0014: br.s IL_0016
IL_0016: ldloc.0
IL_0017: ret

I was worried about callvirt, but it's OK (thanks to constrainted)

I wonder, is there advantage in moving interop code to extension methods? Since the wrapper code is generated, isn't easier to just duplicate the methods in derived types and add implicit/explicit cast operators?

dzmitry-lahoda commented 6 years ago

Not sure I understand whole request fully, but have internal feeling that with new C# features and libraries better interfaces may be achieved. Do not say that next suits or of good performance, but just another view onto related problems.

using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace unsafe_com
{
    public struct MyDevice { }

    public static class Extensions
    {
        ////Error CS1103  The first parameter of an extension method cannot be of type 'MyDevice*'  
        //public static unsafe void Release(this MyDevice* com)
        //{
        //    Debug.WriteLine((IntPtr)com);
        //}

        //public static unsafe void Render(this MyDevice* com)
        //{
        //    Debug.WriteLine((IntPtr)com);
        //}

        public static unsafe void Release(this ref MyDevice com)
        {
            Marshal.Release((IntPtr)Unsafe.AsPointer(ref com));
        }

        private static void Render(IntPtr com)
        {
            Console.WriteLine(com);
        }

        public static unsafe void Render(this ref MyDevice com)
        {
            Render((IntPtr)Unsafe.AsPointer(ref com));
        }

        public unsafe static ref MyDevice InitDevice()
        {
           // just a hack to allocated "com pointer"
            void* someCom = (void*)Marshal.AllocHGlobal(0);
            ref MyDevice x = ref Unsafe.AsRef<MyDevice>(someCom);
            return ref x;
        }
    }

    public static class TestClass
    {
        public static void Test()
        {
            ref var x = ref Extensions.InitDevice();
            x.Render();
            x.Release();
        }
    }
}
dzmitry-lahoda commented 6 years ago

For this seems should ask C# lang - why constraint is possible, but not instantiation? So we may have AddRef/Release/QueryInterface inside Com<T>, but other methods as extensions to be MyDevice* device specific.

using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace unsafe_com_generic
{
    public unsafe struct Com<T> where T:unmanaged, void*
    {

    }

    public struct MyDevice { }

    public static class Extensions
    {

    }

    public static class TestClass
    {
        public static void Test()
        {
            //Error CS0306  The type 'void*' may not be used as a type argument 
            //var t = new Com<void*>();
            //var t2 = new Com<MyDevice*>();
        }
    }
}