3
votes

Documentation about GlobalLock says:

Return value
If the function succeeds, the return value is a pointer to the first byte of the memory block.
If the function fails, the return value is NULL. To get extended error information, call GetLastError.

Remarks
Each successful call that a process makes to GlobalLock for an object must be matched by a corresponding call to GlobalUnlock.
....
If the specified memory block has been discarded or if the memory block has a zero-byte size, this function returns NULL.

So, as we see, GlobalLock() could return NULL if error or memory block size has zero-byte size.
On the other hand, GlobalUnlock() should be called ONLY if GlobalLock() was successful. So, how correctly define case when GlobalUnlock() should be called? What approach is correct from following variants and why?

Variant 0:

HGLOBAL hMem = /*some handle on global memory block*/;

// lock block
auto pMem = static_cast<LPBYTE>(::GlobalLock(hMem));
if (pMem!=nullptr)
{
   // ... work with pMem

}

// call unlock in any case
::GlobalUnlock(hMem);

Variant 1:

HGLOBAL hMem = /*some handle on global memory block*/;

// lock block
auto pMem = static_cast<LPBYTE>(::GlobalLock(hMem));
if (pMem!=nullptr)
{
   // ... work with pMem

   // unlock block
   ::GlobalUnlock(hMem);
}

Variant 2:

HGLOBAL hMem = /*some handle on global memory block*/;

// lock block
auto pMem = static_cast<LPBYTE>(::GlobalLock(hMem));
auto isMemLocked = (pMem!=nullptr);
if (isMemLocked)
{
   // ... work with pMem
}
else
{
   // is it real error?
   isMemLocked = ::GetLastError()==NOERROR;
}

if (isMemLocked)
{
   // unlock block
   ::GlobalUnlock(hMem);
}

Update: We assume that hMem is valid (handle is not NULL).

P.S.: Great thanks for your answers.

2
@RbMm, thank you, but why not Variant 2? Why do you think that NULL for zero-sized block is not successful call too?23W
I'd highly suggest using an RAII wrapper for this. global_locker that takes in an HGLOBALMgetz

2 Answers

3
votes

from GlobalLock documentation

Each successful call that a process makes to GlobalLock for an object must be matched by a corresponding call to GlobalUnlock.

and

If the function succeeds, the return value is a pointer to the first byte of the memory block.

If the function fails, the return value is NULL

so we need call GlobalUnlock only if previous call to GlobalLock return not NULL

pattern is next:

    if (PVOID pv = GlobalLock(hg))
    {
        //...
        GlobalUnlock(hg);
    }

in case we try do GlobalLock on memory block which has a zero-byte size - we always got 0 and ERROR_DISCARDED. we not need call GlobalUnlock in this case - it simply return ERROR_NOT_LOCKED in this case.


if look from c++ perspective GlobalAlloc with GMEM_MOVEABLE flag return ~ weak_ptr - so HGLOBAL by fact point to object like weak_ptr in this case. the GlobalLock(hg) is analog of weak_ptr::lock which return shared_ptr (direct pointer to actual memory block). and GlobalLock is analog of release this shared_ptr. after call GlobalDiscard on HGLOBAL hg - shared_ptr (real memory block) will be destroyed. but HGLOBAL hg (weak_ptr) still will be valid, simply every GlobalLock(hg) (weak_ptr::lock) call on it fail with error ERROR_DISCARDED. finally GlobalFree delete this weak_ptr. demo code:

if (HGLOBAL hg = GlobalAlloc(GMEM_MOVEABLE, 8))
{
    if (PVOID pv = GlobalLock(hg))
    {
        ASSERT(!GlobalDiscard(hg));

        GlobalUnlock(hg);
    }

    ASSERT(GlobalDiscard(hg));

    ASSERT(!GlobalLock(hg));

    ASSERT(GetLastError() == ERROR_DISCARDED);

    ASSERT(!GlobalUnlock(hg));

    ASSERT(GetLastError() == ERROR_NOT_LOCKED);

    GlobalFree(hg);
}

if (HGLOBAL hg = GlobalAlloc(GMEM_MOVEABLE, 0))
{
    ASSERT(!GlobalLock(hg));

    ASSERT(GetLastError() == ERROR_DISCARDED);

    ASSERT(!GlobalUnlock(hg));

    ASSERT(GetLastError() == ERROR_NOT_LOCKED);

    GlobalFree(hg);
}
0
votes
if (PVOID p = GlobalLock(hGlob))
{
  ...
  GlobalUnlock(hGlob);
}

is the correct pattern and answered by RbMm but variant 0 is also accepted by Windows because GlobalUnlock(NULL) returns TRUE without doing anything else. This is of course a undocumented implementation detail and I only verified this on Windows NT 4 and Windows 8 but I assume everything in between acts the same.

This happens because Windows uses certain tag bits and alignment to tell if the HGLOBAL is fixed or moveable memory and NULL obviously has no tag bits set so GlobalUnlock just returns.

There is no reason to use this alternative pattern because:

  1. You would be relying on implementation details.
  2. You cannot omit the GlobalLock return value check unless you know that the HGLOBAL is fixed memory and in that case you can omit all the locking/unlocking because it is pointless overhead if you are only using fixed memory.