bRad Gibson bRad Gibson - 2 months ago 13
C++ Question

What is the safest way to pass a unique_ptr through a function requiring a void* param?

I am creating a thread under Windows which requires a parameter block to be passed as a void* (LPVOID).

The calling function heap-allocates the parameter block, since the spawned thread may outlive the calling function. The calling function does not need to share ownership of the parameter block.

Here is my thinking so far:

...
using Functor = std::function<void()>

class SpawnData
{
public:
SpawnData(Functor func) : func(func) {}
Functor func;
};

DWORD WINAPI MyTaskLauncher(LPVOID ppRawData)
{
assert(ppRawData != nullptr);
//auto pData = *static_cast<std::unique_ptr<SpawnData>*>(ppRawData); //not permitted (A)
auto ppData = static_cast<std::unique_ptr<SpawnData>*>(ppRawData); //Feels safe, but...
assert(*ppRawData != nullptr);
(*ppRawData)->func(); //could dereference invalid memory?
}

ThreadId MyThreadSpawner(Functor func)
{
auto pData = std::make_unique<SpawnData>(func);

//CreateThread() is Windows API with a void* signature for the data param
... = CreateThread(..., static_cast<LPTHREAD_START_ROUTINE>(MyTaskLauncher),
static_cast<LPVOID>(&(pData.get())), ...); //Ugh... :(

threadId = ...;
return threadId;
}


My questions:

1) Would you agree that there is a race condition starting at the call to CreateThread() for MyTaskLauncher to re-wrap the raw pointer before MyThreadSpawner() exits?

2) Would you agree that Q1) is essentially moot because MyThreadSpawner()'s pData will be destroyed when it goes out of scope regardless of whether MyTaskLauncher had already "wrapped" its memory or not?

3) What's the safest way to strip, pass and re-wrap on the other side my smart pointer through the CreateThread() API?

Consider the following:

DWORD WINAPI MyTaskLauncher(LPVOID pRawData)
{
assert(pRawData != null)
auto pData = std::make_unique<SpawnData>(*static_cast<SpawnData*>(pRawData));
//signal MyThreadSpawner() that SpawnData is safely wrapped
//...do work...
return result;
}

ThreadId MyThreadSpawner(Functor func)
{
auto pData = std::make_unique<SpawnData>(func);

//CreateThread() is Windows API with a void* signature for the data param
... = CreateThread(..., static_cast<LPTHREAD_START_ROUTINE>(MyTaskLauncher),
static_cast<LPVOID>(pData.get()), ...); //Hmm...

threadId = ...;
//Wait for MyTaskLauncher to signal SpawnData is safely wrapped
return threadId;
}


4) Is this legal? For a time, there will be two, er, unique_ptrs pointing to the very same memory...

If I update this to use shared_ptr as follows:

//MyTaskLauncher:
...
auto pData = *static_cast<std::shared_ptr<SpawnData>*>(pRawData)); // (B)
...

//MyThreadSpawner:
auto pData = std::make_shared<SpawnData>(func);
...


5) The line marked (B), above is legal, whereas (A) far above (which does the same to a
std::unique_ptr<SpawnData>*
), is not. Can anyone shed some light on as to why?

6) And finally, any suggestions on simpler and/or safer techniques for passing safe data through a function signature requiring a void*?

Thanks in advance for your thoughts.

Answer

2) Would you agree that Q1) is essentially moot because MyThreadSpawner()'s pData will be destroyed when it goes out of scope regardless of whether MyTaskLauncher had already "wrapped" its memory or not?

Yes. That's what a unique_ptr is; it represents unique ownership. You're trying to break that.

3) What's the safest way to strip, pass and re-wrap on the other side my smart pointer through the CreateThread() API?

Define "safest". It's not clear why MyThreadSpawner is using a unique_ptr at all.

You are trying to transfer ownership of a unique_ptr. So you need to actually do that; the sender must lose ownership and the receiver must acquire it. That's pretty trivial:

DWORD WINAPI MyTaskLauncher(LPVOID pData)
{
    assert(pData!= nullptr);
    //Gain ownership of memory.
    unique_ptr<SpawnData> pSpawnData(static_cast<SpawnData*>(pData));
    pSpawnData->func();
}

ThreadId MyThreadSpawner(Functor func)
{
    auto pData = std::make_unique<SpawnData>(func);

    //CreateThread() is Windows API with a void* signature for the data param
    ... = CreateThread(..., static_cast<LPTHREAD_START_ROUTINE>(MyTaskLauncher),
                      //Lose ownership of memory.
                      static_cast<LPVOID>(pData.release()), ...);     
    threadId = ...;
    return threadId;
}

5) The line marked (B), above is legal, whereas (A) far above (which does the same to a std::unique_ptr*), is not. Can anyone shed some light on as to why?

Legal in what sense? The void* does not point to a shared_ptr of any sort. Therefore casting it to a shared_ptr and then accessing it is not legal C++.

Just because your compiler just so happened to let you do it doesn't make it legal.

And finally, any suggestions on simpler and/or safer techniques for passing safe data through a function signature requiring a void*?

Yes: use std::thread. You can get a Win32 thread handle via thread::native_handle.

Comments