Marshal.AllocHGlobal(0) - Why does this not return IntPtr.Zero

Viewed 1035

Just trying to understand if this makes any sense and where the sense in it might be.

Marshal.AllocHGlobal(int cb) allocates the specified number of bytes in unmanaged memory.

But why does Marshal.AllocHGlobal(0) actually return an IntPtr that is not IntPtr.Zero? And am I supposed to free the allocated 0 bytes when I'm done using 0 bytes?

I can't see the logic behind this implementation, can someone explain it?

2 Answers

1. Why does Marshal.AllocHGlobal not return IntPtr.Zero if 0 bytes are allocated?

Marshal.AllocHGlobal internally calls the WinAPI function LocalAlloc from WinBase.h.

As to why Marshal.AllocHGlobal(0) does not return IntPtr.Zero: LocalAlloc only returns NULL (C# equivalent: IntPtr.Zero) in case of a failure during the allocation. This can also be seen in the source code:

IntPtr pNewMem = Win32Native.LocalAlloc_NoSafeHandle(LMEM_FIXED, unchecked(numBytes));

if (pNewMem == IntPtr.Zero) {
    throw new OutOfMemoryException();
}
return pNewMem;

2. Why does allocating 0 bytes return a (valid) memory adress?

The documentation says about the return value of LocalAlloc:

If the function succeeds, the return value is a handle to the newly allocated memory object.

If the function fails, the return value is NULL.

Now, LocalAlloc fails only if‡ uBytes is negative; it has no problem with positive or zero values.

What that means is that allocation will always succeed‡ and you will always receive a valid pointer if you try to allocate 0 bytes.

‡ There are other reasons for failing, e.g. insufficient memory. For simplicity, they were left out in this explanation.


3. Should I free the memory allocated by Marshal.AllocHGlobal(0)?

The signature of LocalAlloc is this:

DECLSPEC_ALLOCATOR HLOCAL LocalAlloc(
  UINT   uFlags,
  SIZE_T uBytes
);

The documentation states that

if [uBytes] is zero and the uFlags parameter specifies LMEM_MOVEABLE, the function returns a handle to a memory object that is marked as discarded.

For some reason, Marshal.AllocHGlobal(0) does not pass LMEM_MOVEABLE but rather LMEM_FIXED instead.

The documentation lacks information on this specific case. Running tests (see below) have shown that memory is in fact being allocated, and you definitely need to free the memory as shown below:

IntPtr zeroBytesPtr = Marshal.AllocHGlobal(0);

// Do stuff with the pointer.

Marshal.FreeHGlobal(zeroBytesPtr);

If Marshal.AllocHGlobal passed LMEM_MOVEABLE instead, there would not be the need to free the pointer anywhere.


As for the tests:

while(true) {
    void* v = LocalAlloc(LMEM_FIXED, 0);
}

allocates memory for every iteration of the loop and returns a new address each time, while

while(true) {
    void* v = LocalAlloc(LMEM_MOVEABLE, 0);
}

allocates memory only once and returns the same address every time.

This indicates why the memory allocated by Marshal.AllocHGlobal must be freed (as it uses LMEM_FIXED), because a new memory object is allocated with each call.

It may be important for some use cases that two distinct calls to AllocHGlobal never return the same IntPtr value (absent any FreeHGlobal calls), even if two of the calls happen to specify a somewhat nonsense size value.

At the end of the day, you're calling this function presumably for interop purposes with unmanaged code that is expecting to work with the "global" heap. And GlobalAlloc has long been stated to accept a 0 value, and the function always actually performs some allocation (if it succeeds).

Related