feat(shm): implement reference counting for Windows shared memory

Problem:
- Reference counting tests fail on Windows (ReleaseMemory, ReferenceCount,
  SubtractReference, HandleRef, HandleSubRef)
- get_ref() and sub_ref() were stub implementations returning 0/doing nothing
- CreateFileMapping HANDLE lacks built-in reference counting mechanism

Solution:
- Implement reference counting using std::atomic<std::int32_t> stored at
  the end of shared memory (same strategy as POSIX version)
- Add calc_size() helper to allocate extra space for atomic counter
- Add acc_of() helper to access the atomic counter at the end of memory
- Modify acquire() to allocate calc_size(size) instead of size
- Modify get_mem() to initialize counter to 1 on first mapping
- Modify release() to decrement counter and return ref count before decrement
- Implement get_ref() to return current reference count
- Implement sub_ref() to atomically decrement reference count
- Convert file from Windows (CRLF) to Unix (LF) line endings for consistency

Key Implementation Details:
1. Reference counter stored at end of shared memory (aligned to info_t)
2. First get_mem() call: fetch_add(1) initializes counter to 1
3. release() returns ref count before decrement (for semantics compatibility)
4. Memory layout: [user data][padding][atomic<int32_t> counter]
5. Uses memory_order_acquire/release/acq_rel for proper synchronization

This makes Windows implementation match POSIX behavior and ensures all
reference counting tests pass on Windows platform.
This commit is contained in:
木头云 2025-11-30 10:55:24 +00:00
parent b9dd75ccd9
commit 7726742157

View File

@ -1,144 +1,188 @@
#if defined(__MINGW32__) #if defined(__MINGW32__)
#include <windows.h> #include <windows.h>
#else #else
#include <Windows.h> #include <Windows.h>
#endif #endif
#include <string> #include <atomic>
#include <utility> #include <string>
#include <utility>
#include "libipc/shm.h"
#include "libipc/def.h" #include "libipc/shm.h"
#include "libipc/pool_alloc.h" #include "libipc/def.h"
#include "libipc/pool_alloc.h"
#include "libipc/utility/log.h"
#include "libipc/memory/resource.h" #include "libipc/utility/log.h"
#include "libipc/memory/resource.h"
#include "to_tchar.h"
#include "get_sa.h" #include "to_tchar.h"
#include "get_sa.h"
namespace {
namespace {
struct id_info_t {
HANDLE h_ = NULL; struct info_t {
void* mem_ = nullptr; std::atomic<std::int32_t> acc_;
std::size_t size_ = 0; };
};
struct id_info_t {
} // internal-linkage HANDLE h_ = NULL;
void* mem_ = nullptr;
namespace ipc { std::size_t size_ = 0;
namespace shm { };
id_t acquire(char const * name, std::size_t size, unsigned mode) { constexpr std::size_t calc_size(std::size_t size) {
if (!is_valid_string(name)) { return ((((size - 1) / alignof(info_t)) + 1) * alignof(info_t)) + sizeof(info_t);
ipc::error("fail acquire: name is empty\n"); }
return nullptr;
} inline auto& acc_of(void* mem, std::size_t size) {
HANDLE h; return reinterpret_cast<info_t*>(static_cast<ipc::byte_t*>(mem) + size - sizeof(info_t))->acc_;
auto fmt_name = ipc::detail::to_tchar(name); }
// Opens a named file mapping object.
if (mode == open) { } // internal-linkage
h = ::OpenFileMapping(FILE_MAP_ALL_ACCESS, FALSE, fmt_name.c_str());
if (h == NULL) { namespace ipc {
ipc::error("fail OpenFileMapping[%d]: %s\n", static_cast<int>(::GetLastError()), name); namespace shm {
return nullptr;
} id_t acquire(char const * name, std::size_t size, unsigned mode) {
} if (!is_valid_string(name)) {
// Creates or opens a named file mapping object for a specified file. ipc::error("fail acquire: name is empty\n");
else { return nullptr;
h = ::CreateFileMapping(INVALID_HANDLE_VALUE, detail::get_sa(), PAGE_READWRITE | SEC_COMMIT, }
0, static_cast<DWORD>(size), fmt_name.c_str()); HANDLE h;
DWORD err = ::GetLastError(); auto fmt_name = ipc::detail::to_tchar(name);
// If the object exists before the function call, the function returns a handle to the existing object // Opens a named file mapping object.
// (with its current size, not the specified size), and GetLastError returns ERROR_ALREADY_EXISTS. if (mode == open) {
if ((mode == create) && (err == ERROR_ALREADY_EXISTS)) { h = ::OpenFileMapping(FILE_MAP_ALL_ACCESS, FALSE, fmt_name.c_str());
if (h != NULL) ::CloseHandle(h); if (h == NULL) {
h = NULL; ipc::error("fail OpenFileMapping[%d]: %s\n", static_cast<int>(::GetLastError()), name);
} return nullptr;
if (h == NULL) { }
ipc::error("fail CreateFileMapping[%d]: %s\n", static_cast<int>(err), name); }
return nullptr; // Creates or opens a named file mapping object for a specified file.
} else {
} std::size_t alloc_size = calc_size(size);
auto ii = mem::alloc<id_info_t>(); h = ::CreateFileMapping(INVALID_HANDLE_VALUE, detail::get_sa(), PAGE_READWRITE | SEC_COMMIT,
ii->h_ = h; 0, static_cast<DWORD>(alloc_size), fmt_name.c_str());
ii->size_ = size; DWORD err = ::GetLastError();
return ii; // If the object exists before the function call, the function returns a handle to the existing object
} // (with its current size, not the specified size), and GetLastError returns ERROR_ALREADY_EXISTS.
if ((mode == create) && (err == ERROR_ALREADY_EXISTS)) {
std::int32_t get_ref(id_t) { if (h != NULL) ::CloseHandle(h);
return 0; h = NULL;
} }
if (h == NULL) {
void sub_ref(id_t) { ipc::error("fail CreateFileMapping[%d]: %s\n", static_cast<int>(err), name);
// Do Nothing. return nullptr;
} }
}
void * get_mem(id_t id, std::size_t * size) { auto ii = mem::alloc<id_info_t>();
if (id == nullptr) { ii->h_ = h;
ipc::error("fail get_mem: invalid id (null)\n"); ii->size_ = size;
return nullptr; return ii;
} }
auto ii = static_cast<id_info_t*>(id);
if (ii->mem_ != nullptr) { std::int32_t get_ref(id_t id) {
if (size != nullptr) *size = ii->size_; if (id == nullptr) {
return ii->mem_; return 0;
} }
if (ii->h_ == NULL) { auto ii = static_cast<id_info_t*>(id);
ipc::error("fail to_mem: invalid id (h = null)\n"); if (ii->mem_ == nullptr || ii->size_ == 0) {
return nullptr; return 0;
} }
LPVOID mem = ::MapViewOfFile(ii->h_, FILE_MAP_ALL_ACCESS, 0, 0, 0); return acc_of(ii->mem_, calc_size(ii->size_)).load(std::memory_order_acquire);
if (mem == NULL) { }
ipc::error("fail MapViewOfFile[%d]\n", static_cast<int>(::GetLastError()));
return nullptr; void sub_ref(id_t id) {
} if (id == nullptr) {
MEMORY_BASIC_INFORMATION mem_info; ipc::error("fail sub_ref: invalid id (null)\n");
if (::VirtualQuery(mem, &mem_info, sizeof(mem_info)) == 0) { return;
ipc::error("fail VirtualQuery[%d]\n", static_cast<int>(::GetLastError())); }
return nullptr; auto ii = static_cast<id_info_t*>(id);
} if (ii->mem_ == nullptr || ii->size_ == 0) {
ii->mem_ = mem; ipc::error("fail sub_ref: invalid id (mem = %p, size = %zd)\n", ii->mem_, ii->size_);
ii->size_ = static_cast<std::size_t>(mem_info.RegionSize); return;
if (size != nullptr) *size = ii->size_; }
return static_cast<void *>(mem); acc_of(ii->mem_, calc_size(ii->size_)).fetch_sub(1, std::memory_order_acq_rel);
} }
std::int32_t release(id_t id) noexcept { void * get_mem(id_t id, std::size_t * size) {
if (id == nullptr) { if (id == nullptr) {
ipc::error("fail release: invalid id (null)\n"); ipc::error("fail get_mem: invalid id (null)\n");
return -1; return nullptr;
} }
auto ii = static_cast<id_info_t*>(id); auto ii = static_cast<id_info_t*>(id);
if (ii->mem_ == nullptr || ii->size_ == 0) { if (ii->mem_ != nullptr) {
ipc::error("fail release: invalid id (mem = %p, size = %zd)\n", ii->mem_, ii->size_); if (size != nullptr) *size = ii->size_;
} return ii->mem_;
else ::UnmapViewOfFile(static_cast<LPCVOID>(ii->mem_)); }
if (ii->h_ == NULL) { if (ii->h_ == NULL) {
ipc::error("fail release: invalid id (h = null)\n"); ipc::error("fail to_mem: invalid id (h = null)\n");
} return nullptr;
else ::CloseHandle(ii->h_); }
mem::free(ii); LPVOID mem = ::MapViewOfFile(ii->h_, FILE_MAP_ALL_ACCESS, 0, 0, 0);
return 0; if (mem == NULL) {
} ipc::error("fail MapViewOfFile[%d]\n", static_cast<int>(::GetLastError()));
return nullptr;
void remove(id_t id) noexcept { }
if (id == nullptr) { MEMORY_BASIC_INFORMATION mem_info;
ipc::error("fail release: invalid id (null)\n"); if (::VirtualQuery(mem, &mem_info, sizeof(mem_info)) == 0) {
return; ipc::error("fail VirtualQuery[%d]\n", static_cast<int>(::GetLastError()));
} return nullptr;
release(id); }
} std::size_t actual_size = static_cast<std::size_t>(mem_info.RegionSize);
if (ii->size_ == 0) {
void remove(char const * name) noexcept { // Opening existing shared memory
if (!is_valid_string(name)) { ii->size_ = actual_size - sizeof(info_t);
ipc::error("fail remove: name is empty\n"); }
return; else {
} // Should match the size we allocated in acquire
// Do Nothing. ii->size_ = ii->size_; // Keep user-requested size
} }
ii->mem_ = mem;
} // namespace shm if (size != nullptr) *size = ii->size_;
} // namespace ipc // Initialize or increment reference counter
acc_of(mem, calc_size(ii->size_)).fetch_add(1, std::memory_order_release);
return static_cast<void *>(mem);
}
std::int32_t release(id_t id) noexcept {
if (id == nullptr) {
ipc::error("fail release: invalid id (null)\n");
return -1;
}
std::int32_t ret = -1;
auto ii = static_cast<id_info_t*>(id);
if (ii->mem_ == nullptr || ii->size_ == 0) {
ipc::error("fail release: invalid id (mem = %p, size = %zd)\n", ii->mem_, ii->size_);
}
else {
ret = acc_of(ii->mem_, calc_size(ii->size_)).fetch_sub(1, std::memory_order_acq_rel);
::UnmapViewOfFile(static_cast<LPCVOID>(ii->mem_));
}
if (ii->h_ == NULL) {
ipc::error("fail release: invalid id (h = null)\n");
}
else ::CloseHandle(ii->h_);
mem::free(ii);
return ret;
}
void remove(id_t id) noexcept {
if (id == nullptr) {
ipc::error("fail release: invalid id (null)\n");
return;
}
release(id);
}
void remove(char const * name) noexcept {
if (!is_valid_string(name)) {
ipc::error("fail remove: name is empty\n");
return;
}
// Do Nothing.
}
} // namespace shm
} // namespace ipc