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.