Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 15 additions & 14 deletions CMakePresets.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"installDir": "${sourceDir}/build/install/${presetName}",
"toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake",
"cacheVariables": {
"WIL_ENABLE_ASAN": true,
"CMAKE_CONFIGURATION_TYPES": "Debug;RelWithDebInfo;Release;MinSizeRel",
"CMAKE_CXX_COMPILER": "cl",
"CMAKE_C_COMPILER": "cl"
Expand All @@ -29,29 +30,33 @@
}
},
"cacheVariables": {
"WIL_ENABLE_ASAN": false,
"CMAKE_CXX_COMPILER": "clang-cl",
"CMAKE_C_COMPILER": "clang-cl"
}
},
{
"name": "clang-release",
"inherits": "clang",
"hidden": false,
"cacheVariables": {
"WIL_ENABLE_ASAN": true,
"WIL_ENABLE_UBSAN": true
}
}
],
"buildPresets": [
{
"name": "msvc-debug",
"displayName": "MSVC Debug",
"configurePreset": "msvc",
"configuration": "Debug",
"cacheVariables": {
"WIL_ENABLE_ASAN": true
}
"configuration": "Debug"
},
{
"name": "msvc-release",
"displayName": "MSVC Release (debuggable)",
"configurePreset": "msvc",
"configuration": "RelWithDebInfo",
"cacheVariables": {
"WIL_ENABLE_ASAN": true
}
"configuration": "RelWithDebInfo"
},
{
"name": "clang-debug",
Expand All @@ -62,12 +67,8 @@
{
"name": "clang-release",
"displayName": "clang Release (debuggable)",
"configurePreset": "clang",
"configuration": "RelWithDebInfo",
"cacheVariables": {
"WIL_ENABLE_ASAN": true,
"WIL_ENABLE_UBSAN": true
}
"configurePreset": "clang-release",
"configuration": "RelWithDebInfo"
}
],
"testPresets": [
Expand Down
1 change: 1 addition & 0 deletions _codeql_detected_source_root
106 changes: 62 additions & 44 deletions include/wil/result.h
Original file line number Diff line number Diff line change
Expand Up @@ -375,73 +375,91 @@ namespace details_abi
template <typename T>
class ThreadLocalStorage
{
public:
ThreadLocalStorage(const ThreadLocalStorage&) = delete;
ThreadLocalStorage& operator=(const ThreadLocalStorage&) = delete;

ThreadLocalStorage() = default;
struct Node
{
Node* next{nullptr};
DWORD threadId = 0xffffffffU;
T value{};
};

~ThreadLocalStorage() WI_NOEXCEPT
struct Bucket
{
for (auto& entry : m_hashArray)
wil::srwlock lock;
Node* head{nullptr};

~Bucket() WI_NOEXCEPT
{
Node* pNode = entry;
while (pNode != nullptr)
// Cleanup in a loop rather than recursively
while (head)
{
auto pCurrent = pNode;
#pragma warning(push)
#pragma warning(disable : 6001) // https://github.com/microsoft/wil/issues/164
pNode = pNode->pNext;
#pragma warning(pop)
pCurrent->~Node();
::HeapFree(::GetProcessHeap(), 0, pCurrent);
auto tmp = head;
head = tmp->next;
tmp->~Node();
details::FreeProcessHeap(tmp);
}
entry = nullptr;
}
}
};

Bucket m_hashArray[13]{};

public:
ThreadLocalStorage(const ThreadLocalStorage&) = delete;
ThreadLocalStorage& operator=(const ThreadLocalStorage&) = delete;

ThreadLocalStorage() = default;
~ThreadLocalStorage() WI_NOEXCEPT = default;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
~ThreadLocalStorage() WI_NOEXCEPT = default;
~ThreadLocalStorage() = default;

noexcept is the default

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lol. Thanks Copilot. Teaches me to review closer.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be fair, it was like this before, so not necessarily Copilot's fault (this time)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that there is /Zc:implicitNoexcept- which disables the implicit noexcept on destructors.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If folks are disabling language features, this one instance isn't going to have much of a relative impact on them...


// Note: Can return nullptr even when (shouldAllocate == true) upon allocation failure
T* GetLocal(bool shouldAllocate = false) WI_NOEXCEPT
{
// Get the current thread ID
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like a pretty unnecessary comment

DWORD const threadId = ::GetCurrentThreadId();

// Determine the appropriate bucket for this thread
size_t const index = ((threadId >> 2) % ARRAYSIZE(m_hashArray)); // Reduce hash collisions; thread IDs are even.
for (auto pNode = m_hashArray[index]; pNode != nullptr; pNode = pNode->pNext)
Bucket& bucket = m_hashArray[index];

// Lock the bucket and search for an existing entry
{
if (pNode->threadId == threadId)
auto lock = bucket.lock.lock_shared();
for (auto pNode = bucket.head; pNode != nullptr; pNode = pNode->next)
{
return &pNode->value;
if (pNode->threadId == threadId)
{
return &pNode->value;
}
}
}

if (shouldAllocate)
if (!shouldAllocate)
{
if (auto pNewRaw = details::ProcessHeapAlloc(0, sizeof(Node)))
{
auto pNew = new (pNewRaw) Node{threadId};
return nullptr;
}

Node* pFirst;
do
{
pFirst = m_hashArray[index];
pNew->pNext = pFirst;
} while (::InterlockedCompareExchangePointer(reinterpret_cast<PVOID volatile*>(m_hashArray + index), pNew, pFirst) !=
pFirst);
// No entry for us, make a new one and insert it at the head
void* newNodeStore = details::ProcessHeapAlloc(0, sizeof(Node));
if (!newNodeStore)
{
return nullptr;
}
auto node = new (newNodeStore) Node{nullptr, threadId};

return &pNew->value;
// Look again and insert the new node
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's looking for a matching thread id... Unless we're worried about interrupts (in which case deadlock is now on the table), this additional check seems unnecessary since the same thread can't race against itself.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, good point. It can just acquire the lock and shove it into the head.

auto lock = bucket.lock.lock_exclusive();
for (auto pNode = bucket.head; pNode != nullptr; pNode = pNode->next)
{
if (pNode->threadId == threadId)
{
node->~Node();
details::FreeProcessHeap(node);
return &pNode->value;
}
}
return nullptr;
}

private:
struct Node
{
DWORD threadId = 0xffffffffU;
Node* pNext = nullptr;
T value{};
};

Node* volatile m_hashArray[10]{};
node->next = bucket.head;
bucket.head = node;
return &bucket.head->value;
}
};

struct ThreadLocalFailureInfo
Expand Down
62 changes: 62 additions & 0 deletions tests/ResultTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@

#include "common.h"

#include <thread>
#include <vector>
#include <atomic>

static volatile long objectCount = 0;
struct SharedObject
{
Expand Down Expand Up @@ -773,3 +777,61 @@ TEST_CASE("ResultTests::ReportDoesNotChangeLastError", "[result]")
LOG_IF_WIN32_BOOL_FALSE(FALSE);
REQUIRE(::GetLastError() == ERROR_ABIOS_ERROR);
}

TEST_CASE("ResultTests::ThreadLocalStorage", "[result]")
{
constexpr int NUM_THREADS = 10;
constexpr int ITERATIONS = 1000;

wil::details_abi::ThreadLocalStorage<int> storage;
std::atomic<int> errors{0};
std::vector<std::thread> threads;

// Create multiple threads that will access the thread local storage concurrently
for (int i = 0; i < NUM_THREADS; ++i)
{
threads.emplace_back([&storage, &errors, i]() {
for (int j = 0; j < ITERATIONS; ++j)
{
// Get or create thread local value
int* pValue = storage.GetLocal(true);
if (!pValue)
{
errors.fetch_add(1);
continue;
}

// First time should be zero-initialized
if (j == 0 && *pValue != 0)
{
errors.fetch_add(1);
}

// Set a thread-specific value
*pValue = i * 1000 + j;

// Verify we get the same pointer on subsequent calls
int* pValue2 = storage.GetLocal(false);
if (pValue != pValue2)
{
errors.fetch_add(1);
}

// Verify the value is correct
if (*pValue2 != i * 1000 + j)
{
errors.fetch_add(1);
}
}
});
}

// Wait for all threads to complete
for (auto& thread : threads)
{
thread.join();
}

// Verify no errors occurred
REQUIRE(errors.load() == 0);
}