1
\$\begingroup\$

This is a pretty basic class to allocate objects from the native heap. The whole process goes as follows: allocate bytes from NativeMemory.Alloc, set the method table pointer from an existing object, and call the constructor through reflection. Currently, it only works for objects with a parameterless constructor.

The reason for this class is to bring manual memory management to C#. Currently, the only way to allocate objects is from the GC heap which incurs garbage collection penalties. While it does lend a hand to preventing memory leaks, there is a cost to leaving data in memory after it is no longer needed (and why .NET provides utilities like NativeMemory to allocate on the native heap). (SecureString is one example of a class that can't leave it's contents in memory) Of course, this class does not provide any memory safety in the form of leak prevention and requires manual deallocation before all references are deleted. It could of course be used like in C++ with allocations in the constructors and deallocations in destructors (or disposers for C#). You could even create classes for automatic memory management like std::unique_ptr and std::shared_ptr and benefit from semi-managed memory that does not incur GC penalties.

I know that calling the constructor through reflection is an expensive operation, could I get the raw function pointer and call that directly? Also, I suppose I could add parameterized constructors through a similar method, but I would ideally want to reduce the number of gc allocations. This seems to be problematic as the reflective constructor takes in an object[] for the parameters. I have experimented with using a static field for this purpose to keep an allocated object[], but without the thread_local storage class, this seems to be a problem.

I have heard information mentioning an object header at a negative index (for gc). So far, I have not had issues with gc, but I have only done small tests. If a gc was to occur, would it need to access the information or does it just ignore it entirely. Also, would a gc try to deallocate it if it was out of scope (from what I'm reading about mark-sweep algorithms, this doesn't seem likely, but it is Microsoft)?

Enough waiting, here's the code:

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

namespace InfinityOfficialNetwork.Shared.Memory
{
    public sealed class Allocator : IAllocator //the IAllocator is just an interface with the methods listed, it can be removed
    {
        //static methods for generic methods
        private class AllocatorHelper<TAlloc>
        {
            //size of type
            public static readonly unsafe int typeSize = Unsafe.SizeOf<TAlloc>();
            //type fo object
            public static readonly Type objectType;
            //constructor
            public static readonly ConstructorInfo objectConstructor;
            //destructor
            public static readonly MethodInfo objectDestructor;
            //method table pointer, this is needed to call methods from reflection (and anything else listed in the method table)
            public static nint methodTablePointer;

            static unsafe AllocatorHelper()
            {
                //get constructor / destructor
                objectType = typeof(TAlloc);
                objectConstructor = objectType.GetConstructor(Type.EmptyTypes);
                objectDestructor = objectType.GetMethod("Finalize", BindingFlags.NonPublic | BindingFlags.Instance) ?? throw new ArgumentNullException();
                //get an uninitialized instance for the method table pointer
                object obj = RuntimeHelpers.GetUninitializedObject(typeof(TAlloc));
                nint ptr = ((nint*)(&obj))[0];//get the pointer
                methodTablePointer = ptr;
            }
        }

        public unsafe TAlloc* AllocatePointer<TAlloc>() where TAlloc : new()
        {
            //allocate memory
            nint o = Marshal.AllocHGlobal(AllocatorHelper<TAlloc>.typeSize);

            if (o == 0)
                throw new OutOfMemoryException();//should I throw something else?

            //set method table pointer
            ((nint*)o)[0] = AllocatorHelper<TAlloc>.methodTablePointer;

            //call parameterless constructor
            AllocatorHelper<TAlloc>.objectConstructor.Invoke(*(TAlloc*)o, null);


            return (TAlloc*)(o);
        }

        public unsafe TAlloc* AllocateArray<TAlloc>(int Count) where TAlloc : new()
        {
            //does the same thing but for an array of objects
            nint o = Marshal.AllocHGlobal(AllocatorHelper<TAlloc>.typeSize * Count);

            if (o == 0)
                throw new OutOfMemoryException();

            for (int i = 0; i < Count; i++)
            {
                ((nint*)(&(((byte*)o)[AllocatorHelper<TAlloc>.typeSize * i])))[0] = AllocatorHelper<TAlloc>.methodTablePointer;

                AllocatorHelper<TAlloc>.objectConstructor.Invoke(((TAlloc*)o)[i], null);
            }


            return (TAlloc*)(o);
        }
        //returns by reference instead of by ptr
        public unsafe ref TAlloc Allocate<TAlloc>() where TAlloc : new() => ref *AllocatePointer<TAlloc>();

        public unsafe void DeAllocatePointer<TAlloc>(TAlloc* o)
        {
            //call destructor and free
            AllocatorHelper<TAlloc>.objectDestructor.Invoke(*o, null);
            Marshal.FreeHGlobal((nint)o);
        }



        public unsafe void DeAllocateArray<TAlloc>(TAlloc* o, int Count)
        {//same thing but for the entire array
            for (int i = 0; i < Count; i++)
            {
                AllocatorHelper<TAlloc>.objectDestructor.Invoke((o)[i], null);
            }
            Marshal.FreeHGlobal((nint)o);
        }

        //same thing for by-ref objs
        public unsafe void DeAllocate<TAlloc>(ref TAlloc o) => DeAllocatePointer<TAlloc>((TAlloc*)Unsafe.AsPointer(ref o));
    }
}

\$\endgroup\$
6
  • 1
    \$\begingroup\$ what is the purpose of this class? why should it be used instead of stackalloc ? \$\endgroup\$
    – iSR5
    Commented Oct 8 at 9:28
  • \$\begingroup\$ Stackalloc can only be used for value types and only on the stack, this allocates from the native heap and will work with both value and reference types. \$\endgroup\$
    – InfiniPLEX
    Commented Oct 8 at 17:48
  • \$\begingroup\$ Thank you for your clearance , could you please mention that in your post, to give a clear purpose of why you took this road, as this would help improve the collaboration quality (which would be into your advantage). \$\endgroup\$
    – iSR5
    Commented Oct 8 at 18:10
  • \$\begingroup\$ Sure, Ill go ahead and add that. \$\endgroup\$
    – InfiniPLEX
    Commented Oct 8 at 19:43
  • 1
    \$\begingroup\$ Please read the question, I am not using stackalloc at all. \$\endgroup\$
    – InfiniPLEX
    Commented Oct 9 at 18:04

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Browse other questions tagged or ask your own question.