carb/extras/SharedMemory.h
File members: carb/extras/SharedMemory.h
// Copyright (c) 2019-2023, NVIDIA CORPORATION. All rights reserved.
//
// NVIDIA CORPORATION and its licensors retain all intellectual property
// and proprietary rights in and to this software, related documentation
// and any modifications thereto. Any use, reproduction, disclosure or
// distribution of this software and related documentation without an express
// license agreement from NVIDIA CORPORATION is strictly prohibited.
//
#pragma once
#include "../Defines.h"
#include "../Framework.h"
#include "../cpp/Optional.h"
#include "../extras/ScopeExit.h"
#include "../logging/Log.h"
#include "../process/Util.h"
#include "Base64.h"
#include "StringSafe.h"
#include "Unicode.h"
#include <cstddef>
#include <utility>
#if CARB_POSIX
# include <sys/file.h>
# include <sys/mman.h>
# include <sys/stat.h>
# include <sys/syscall.h>
# include <sys/types.h>
# include <cerrno>
# include <fcntl.h>
# include <semaphore.h>
# include <unistd.h>
# if CARB_PLATFORM_LINUX
# include <linux/limits.h> // NAME_MAX
# elif CARB_PLATFORM_MACOS
# include <sys/posix_shm.h>
# endif
#elif CARB_PLATFORM_WINDOWS
# include "../CarbWindows.h"
#endif
namespace carb
{
namespace extras
{
#if !defined(DOXYGEN_SHOULD_SKIP_THIS)
namespace detail
{
# if CARB_POSIX
constexpr int kAllReadWrite = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH;
inline constexpr const char* getGlobalSemaphoreName()
{
// Don't change this as it is completely ABI breaking to do so.
return "/carbonite-sharedmemory";
}
inline void probeSharedMemory()
{
// Probe with a shm_open() call prior to locking the mutex. If the object compiling this does not link librt.so
// then an abort can happen below, but while we have the semaphore locked. Since this is a system-wide
// semaphore, it can leave this code unable to run in the future. Run shm_open here to make sure that it is
// available; if not, an abort will occur but not while we have the system-wide semaphore locked.
shm_open("", 0, 0);
}
class NamedSemaphore
{
public:
NamedSemaphore(const char* name, bool unlinkOnClose = false) : m_name(name), m_unlinkOnClose(unlinkOnClose)
{
m_sema = sem_open(name, O_CREAT, carb::extras::detail::kAllReadWrite, 1);
CARB_FATAL_UNLESS(m_sema, "Failed to create/open shared semaphore {%d/%s}", errno, strerror(errno));
# if CARB_PLATFORM_LINUX
// sem_open() is masked by umask(), so force the permissions with chmod().
// NOTE: This assumes that named semaphores are under /dev/shm and are prefixed with sem. This is not ideal,
// but there does not appear to be any means to translate a sem_t* to a file descriptor (for fchmod())
// or a path.
// NOTE: sem_open() is also affected by umask() on mac, but unfortunately semaphores on mac are not backed
// by the filesystem and can therefore not have their permissions modified after creation.
size_t len = m_name.length() + 12;
char* buf = CARB_STACK_ALLOC(char, len + 1);
extras::formatString(buf, len + 1, "/dev/shm/sem.%s", name + 1); // Skip leading /
chmod(buf, detail::kAllReadWrite);
# endif
}
~NamedSemaphore()
{
int result = sem_close(m_sema);
CARB_ASSERT(result == 0, "Failed to close sema {%d/%s}", errno, strerror(errno));
CARB_UNUSED(result);
if (m_unlinkOnClose)
{
sem_unlink(m_name.c_str());
}
}
bool try_lock()
{
int val = CARB_RETRY_EINTR(sem_trywait(m_sema));
CARB_FATAL_UNLESS(val == 0 || errno == EAGAIN, "sem_trywait() failed {%d/%s}", errno, strerror(errno));
return val == 0;
}
void lock()
{
int result;
# if CARB_PLATFORM_LINUX
auto printMessage = [](const char* format, ...) {
va_list args;
char buffer[1024];
va_start(args, format);
formatStringV(buffer, CARB_COUNTOF(buffer), format, args);
va_end(args);
if (g_carbLogFn && g_carbLogLevel <= logging::kLevelWarn)
{
CARB_LOG_WARN("%s", buffer);
}
else
{
fputs(buffer, stderr);
}
};
constexpr int32_t kTimeoutInSeconds = 5;
struct timespec abstime;
clock_gettime(CLOCK_REALTIME, &abstime);
abstime.tv_sec += kTimeoutInSeconds;
// Since these are global semaphores and a process can crash with them in a bad state, wait for a period of time
// then log so that we have an entry of what's wrong.
result = CARB_RETRY_EINTR(sem_timedwait(m_sema, &abstime));
CARB_FATAL_UNLESS(result == 0 || errno == ETIMEDOUT, "sem_timedwait() failed {%d/%s}", errno, strerror(errno));
if (result == -1 && errno == ETIMEDOUT)
{
printMessage(
"Waiting on global named semaphore %s has taken more than 5 seconds. It may be in a stuck state. "
"You may have to delete /dev/shm/sem.%s and restart the application.",
m_name.c_str(), m_name.c_str() + 1);
CARB_FATAL_UNLESS(
CARB_RETRY_EINTR(sem_wait(m_sema)) == 0, "sem_wait() failed {%d/%s}", errno, strerror(errno));
}
# elif CARB_PLATFORM_MACOS
// mac doesn't support sem_timedwait() and doesn't offer any other named semaphore API
// either. For now we'll just do a blocking wait to attempt to acquire the semaphore.
// If needed, we can add support for the brief wait before warning of a potential hang
// like linux does. It would go something along these lines:
// * spawn a thread or schedule a task to send a SIGUSR2 signal to this thread after
// the given timeout.
// * start an infinite wait on this thread.
// * if SIGUSR2 arrives on this thread and interrupts the infinite wait, print the
// hang warning message then drop into another infinite wait like linux does.
//
// Spawning a new thread for each wait operation here is likely a little heavy handed
// though, especially if there ends up being a lot of contention on this semaphore.
// The alternative would be to have a single shared thread that could handle timeouts
// for multiple other threads.
result = CARB_RETRY_EINTR(sem_wait(m_sema)); // CC-641 to add hang detection
CARB_FATAL_UNLESS(result == 0, "sem_timedwait() failed {%d/%s}", errno, strerror(errno));
# else
CARB_UNSUPPORTED_PLATFORM();
# endif
}
void unlock()
{
CARB_FATAL_UNLESS(CARB_RETRY_EINTR(sem_post(m_sema)) == 0, "sem_post() failed {%d/%s}", errno, strerror(errno));
}
CARB_PREVENT_COPY_AND_MOVE(NamedSemaphore);
private:
sem_t* m_sema;
std::string m_name;
bool m_unlinkOnClose;
};
# endif
} // namespace detail
#endif
class SharedMemory
{
public:
class OpenToken
{
public:
OpenToken() : m_data(nullptr), m_base64(nullptr), m_size(0)
{
}
OpenToken(const char* base64) : m_data(nullptr), m_base64(nullptr), m_size(0)
{
Base64 converter(Base64::Variant::eFilenameSafe);
size_t size;
size_t inSize;
if (base64 == nullptr || base64[0] == 0)
return;
inSize = strlen(base64);
m_base64 = new (std::nothrow) char[inSize + 1];
if (m_base64 != nullptr)
memcpy(m_base64, base64, (inSize + 1) * sizeof(char));
size = converter.getDecodeOutputSize(inSize);
m_data = new (std::nothrow) uint8_t[size];
if (m_data != nullptr)
m_size = converter.decode(base64, inSize, m_data, size);
}
OpenToken(const OpenToken& token) : m_data(nullptr), m_base64(nullptr), m_size(0)
{
*this = token;
}
OpenToken(OpenToken&& token) : m_data(nullptr), m_base64(nullptr), m_size(0)
{
*this = std::move(token);
}
~OpenToken()
{
clear();
}
explicit operator bool() const
{
return isValid();
}
bool operator!() const
{
return !isValid();
}
bool operator==(const OpenToken& token) const
{
if (m_size == 0 && token.m_size == 0)
return true;
if (m_size != token.m_size)
return false;
if (m_data == nullptr || token.m_data == nullptr)
return false;
return memcmp(m_data, token.m_data, m_size) == 0;
}
bool operator!=(const OpenToken& token) const
{
return !(*this == token);
}
OpenToken& operator=(const OpenToken& token)
{
if (this == &token)
return *this;
clear();
if (token.m_data == nullptr)
return *this;
m_data = new (std::nothrow) uint8_t[token.m_size];
if (m_data != nullptr)
{
memcpy(m_data, token.m_data, token.m_size);
m_size = token.m_size;
}
return *this;
}
OpenToken& operator=(OpenToken&& token)
{
if (this == &token)
return *this;
clear();
m_size = token.m_size;
m_data = token.m_data;
m_base64 = token.m_base64;
token.m_size = 0;
token.m_data = nullptr;
token.m_base64 = nullptr;
return *this;
}
const char* getBase64Token()
{
if (m_base64 != nullptr)
return m_base64;
if (m_size == 0)
return nullptr;
Base64 converter(Base64::Variant::eFilenameSafe);
size_t size;
size = converter.getEncodeOutputSize(m_size);
m_base64 = new (std::nothrow) char[size];
if (m_base64 == nullptr)
return nullptr;
converter.encode(m_data, m_size, m_base64, size);
return m_base64;
}
protected:
OpenToken(const void* tokenData, size_t size) : m_data(nullptr), m_base64(nullptr), m_size(0)
{
if (size == 0)
return;
m_data = new (std::nothrow) uint8_t[size];
if (m_data != nullptr)
{
memcpy(m_data, tokenData, size);
m_size = size;
}
}
bool isValid() const
{
return m_data != nullptr && m_size > 0;
}
uint8_t* getToken() const
{
return reinterpret_cast<uint8_t*>(m_data);
}
size_t getSize() const
{
return m_size;
}
void clear()
{
if (m_data != nullptr)
delete[] m_data;
if (m_base64 != nullptr)
delete[] m_base64;
m_size = 0;
m_data = nullptr;
m_base64 = nullptr;
}
uint8_t* m_data;
char* m_base64;
size_t m_size;
friend class SharedMemory;
};
static constexpr uint32_t fCreateMakeUnique = 0x00000001;
static constexpr uint32_t fQuiet = 0x00000002;
static constexpr uint32_t fNoMutexLock = 0x00000004;
enum class AccessMode
{
eDefault,
eReadOnly,
eReadWrite,
};
SharedMemory()
{
m_token = nullptr;
m_access = AccessMode::eDefault;
// collect the system page size and allocation granularity information.
#if CARB_PLATFORM_WINDOWS
m_handle.handleWin32 = nullptr;
CARBWIN_SYSTEM_INFO si;
GetSystemInfo((LPSYSTEM_INFO)&si);
m_pageSize = si.dwPageSize;
m_allocationGranularity = si.dwAllocationGranularity;
#elif CARB_POSIX
m_handle.handleFd = -1;
m_refCount = SEM_FAILED;
m_pageSize = getpagesize();
m_allocationGranularity = m_pageSize;
#else
CARB_UNSUPPORTED_PLATFORM();
#endif
}
~SharedMemory()
{
close();
}
enum Result
{
eError,
eCreated,
eOpened,
};
bool create(const char* name, size_t size, uint32_t flags = fCreateMakeUnique)
{
return createAndOrOpen(name, size, flags, false, true) == eCreated;
}
Result createOrOpen(const char* name, size_t size, uint32_t flags = 0)
{
return createAndOrOpen(name, size, flags, true, true);
}
bool open(const char* name, size_t size, uint32_t flags = 0)
{
return createAndOrOpen(name, size, flags, true, false);
}
bool open(const OpenToken& openToken, AccessMode access = AccessMode::eDefault)
{
OpenTokenImpl* token;
SharedHandle handle;
std::string mappingName;
if (m_token != nullptr)
{
CARB_LOG_ERROR(
"the previous SHM region has not been closed yet. Please close it before opening a new SHM region.");
return false;
}
// not a valid open token => fail.
if (!openToken.isValid())
return false;
token = reinterpret_cast<OpenTokenImpl*>(openToken.getToken());
// make sure the token information seems valid.
if (openToken.getSize() < offsetof(OpenTokenImpl, name) + token->nameLength + 1)
return false;
if (token->size == 0 || token->size % m_pageSize != 0)
return false;
if (access == AccessMode::eDefault)
access = AccessMode::eReadWrite;
token = reinterpret_cast<OpenTokenImpl*>(malloc(openToken.getSize()));
if (token == nullptr)
{
CARB_LOG_ERROR("failed to allocate memory for the open token for this SHM region.");
return false;
}
memcpy(token, openToken.getToken(), openToken.getSize());
mappingName = getPlatformMappingName(token->name);
#if CARB_PLATFORM_WINDOWS
std::wstring fname = carb::extras::convertUtf8ToWide(mappingName.c_str());
handle.handleWin32 =
OpenFileMappingW(getAccessModeFlags(access, FlagType::eFileFlags), CARBWIN_FALSE, fname.c_str());
if (handle.handleWin32 == nullptr)
{
CARB_LOG_ERROR("failed to open a file mapping object with the name '%s' {error = %" PRIu32 "}", token->name,
GetLastError());
free(token);
return false;
}
#elif CARB_POSIX
// create the reference count object. Note that this must already exist in the system
// since we are expecting another process to have already created the region.
if (!initRefCount(token->name, 0, true))
{
CARB_LOG_ERROR("failed to create the reference count object with the name '%s'.", token->name);
free(token);
return false;
}
handle.handleFd = shm_open(mappingName.c_str(), getAccessModeFlags(access, FlagType::eFileFlags), 0);
// failed to open the SHM region => fail.
if (handle.handleFd == -1)
{
CARB_LOG_ERROR("failed to open or create file mapping object with the name '%s' {errno = %d/%s}",
token->name, errno, strerror(errno));
destroyRefCount(token->name);
free(token);
return false;
}
#else
CARB_UNSUPPORTED_PLATFORM();
#endif
m_token = token;
m_handle = handle;
m_access = access;
return true;
}
class View
{
public:
View(View&& view)
{
m_address = view.m_address;
m_size = view.m_size;
m_offset = view.m_offset;
m_pageOffset = view.m_pageOffset;
m_access = view.m_access;
view.init();
}
~View()
{
unmap();
}
View& operator=(View&& view)
{
if (this == &view)
return *this;
unmap();
m_address = view.m_address;
m_size = view.m_size;
m_offset = view.m_offset;
m_pageOffset = view.m_pageOffset;
m_access = view.m_access;
view.init();
return *this;
}
void* getAddress()
{
return reinterpret_cast<void*>(reinterpret_cast<uint8_t*>(m_address) + m_pageOffset);
}
size_t getSize() const
{
return m_size;
}
size_t getOffset() const
{
return m_offset;
}
AccessMode getAccessMode() const
{
return m_access;
}
protected:
// prevent new empty local declarations from being default constructed so that we don't
// need to worry about constantly checking for invalid views.
View()
{
init();
}
// remove these constructors and operators to prevent multiple copies of the same view
// from being created and copied. Doing so could cause other views to be invalidated
// unintentionally if a mapping address is reused (which is common).
View(const View&) = delete;
View& operator=(const View&) = delete;
View& operator=(const View*) = delete;
bool map(SharedHandle handle, size_t offset, size_t size, AccessMode access, size_t allocGran)
{
void* mapPtr = nullptr;
#if CARB_PLATFORM_WINDOWS
size_t granOffset = offset & ~(allocGran - 1);
m_pageOffset = offset - granOffset;
mapPtr = MapViewOfFile(handle.handleWin32, getAccessModeFlags(access, FlagType::eFileFlags),
static_cast<DWORD>(granOffset >> 32), static_cast<DWORD>(granOffset),
static_cast<SIZE_T>(size + m_pageOffset));
if (mapPtr == nullptr)
{
CARB_LOG_ERROR(
"failed to map %zu bytes from offset %zu {error = %" PRIu32 "}", size, offset, GetLastError());
return false;
}
#elif CARB_POSIX
CARB_UNUSED(allocGran);
m_pageOffset = 0;
mapPtr = mmap(
nullptr, size, getAccessModeFlags(access, FlagType::ePageFlags), MAP_SHARED, handle.handleFd, offset);
if (mapPtr == MAP_FAILED)
{
CARB_LOG_ERROR(
"failed to map %zu bytes from offset %zu {errno = %d/%s}", size, offset, errno, strerror(errno));
return false;
}
#else
CARB_UNSUPPORTED_PLATFORM();
#endif
m_address = mapPtr;
m_size = size;
m_offset = offset;
m_access = access;
return true;
}
void unmap()
{
if (m_address == nullptr)
return;
#if CARB_PLATFORM_WINDOWS
if (UnmapViewOfFile(m_address) == CARBWIN_FALSE)
CARB_LOG_ERROR("failed to unmap the region at %p {error = %" PRIu32 "}", m_address, GetLastError());
#elif CARB_POSIX
if (munmap(m_address, m_size) == -1)
CARB_LOG_ERROR("failed to unmap the region at %p {errno = %d/%s}", m_address, errno, strerror(errno));
#else
CARB_UNSUPPORTED_PLATFORM();
#endif
init();
}
void init()
{
m_address = nullptr;
m_size = 0;
m_offset = 0;
m_pageOffset = 0;
m_access = AccessMode::eDefault;
}
void* m_address;
size_t m_size;
size_t m_offset;
size_t m_pageOffset;
AccessMode m_access;
// The SharedMemory object that creates this view needs to be able to call into map().
friend class SharedMemory;
};
View* createView(size_t offset = 0, size_t size = 0, AccessMode access = AccessMode::eDefault) const
{
View* view;
// no SHM region is open -> nothing to do => fail.
if (m_token == nullptr)
return nullptr;
// the requested offset is beyond the region => fail.
if (offset >= m_token->size)
return nullptr;
if (access == AccessMode::eDefault)
access = m_access;
// attempting to map a read/write region on a read-only mapping => fail.
else if (access == AccessMode::eReadWrite && m_access == AccessMode::eReadOnly)
return nullptr;
offset = alignPageFloor(offset);
if (size == 0)
size = m_token->size;
if (offset + size > m_token->size)
size = m_token->size - offset;
view = new (std::nothrow) View();
if (view == nullptr)
return nullptr;
if (!view->map(m_handle, offset, size, access, m_allocationGranularity))
{
delete view;
return nullptr;
}
return view;
}
void close(bool forceUnlink = false)
{
if (m_token == nullptr)
return;
#if CARB_PLATFORM_WINDOWS
CARB_UNUSED(forceUnlink);
if (m_handle.handleWin32 != nullptr)
CloseHandle(m_handle.handleWin32);
m_handle.handleWin32 = nullptr;
#elif CARB_POSIX
if (m_handle.handleFd != -1)
::close(m_handle.handleFd);
m_handle.handleFd = -1;
// check that all references to the SHM region have been released before unlinking
// the named filesystem reference to it. The reference count semaphore can also
// be unlinked from the filesystem at this point.
if (releaseRef() || forceUnlink)
{
std::string mappingName = getPlatformMappingName(m_token->name);
shm_unlink(mappingName.c_str());
destroyRefCount(m_token->name);
}
// close our local reference to the ref count semaphore.
sem_close(m_refCount);
m_refCount = SEM_FAILED;
#else
CARB_UNSUPPORTED_PLATFORM();
#endif
free(m_token);
m_token = nullptr;
m_access = AccessMode::eDefault;
}
bool isOpen() const
{
return m_token != nullptr;
}
OpenToken getOpenToken()
{
if (m_token == nullptr)
return OpenToken();
return OpenToken(m_token, offsetof(OpenTokenImpl, name) + m_token->nameLength + 1);
}
size_t getSize() const
{
if (m_token == nullptr)
return 0;
return m_token->size;
}
AccessMode getAccessMode() const
{
if (m_token == nullptr)
return AccessMode::eDefault;
return m_access;
}
size_t getSystemPageSize() const
{
return m_pageSize;
}
size_t getSystemAllocationGranularity() const
{
return m_allocationGranularity;
}
private:
CARB_IGNOREWARNING_MSC_WITH_PUSH(4200) // nonstandard extension used: zero-sized array in struct/union
#pragma pack(push, 1)
struct OpenTokenImpl
{
size_t size;
uint16_t nameLength;
char name[0];
};
#pragma pack(pop)
CARB_IGNOREWARNING_MSC_POP
enum class FlagType
{
eFileFlags,
ePageFlags,
};
#if CARB_POSIX
struct SemLockGuard
{
sem_t* mutex_;
SemLockGuard(sem_t* mutex) : mutex_(mutex)
{
CARB_FATAL_UNLESS(
CARB_RETRY_EINTR(sem_wait(mutex_)) == 0, "sem_wait() failed {errno = %d/%s}", errno, strerror(errno));
}
~SemLockGuard()
{
CARB_FATAL_UNLESS(
CARB_RETRY_EINTR(sem_post(mutex_)) == 0, "sem_post() failed {errno = %d/%s}", errno, strerror(errno));
}
};
#endif
size_t alignPageCeiling(size_t size) const
{
size_t pageSize = getSystemPageSize();
return (size + (pageSize - 1)) & ~(pageSize - 1);
}
size_t alignPageFloor(size_t size) const
{
size_t pageSize = getSystemPageSize();
return size & ~(pageSize - 1);
}
std::string getPlatformMappingName(const char* name, size_t maxLength = 0)
{
std::string fname;
#if CARB_PLATFORM_WINDOWS
const char prefix[] = "Local\\";
// all named handle objects have a hard undocumented name length limit of 64KB. This is
// due to all ntdll strings using a WORD value as the length in the UNICODE_STRING struct
// that is always used for names at the ntdll level. Any names that are beyond this
// limit get silently truncated and used as-is. This length limit includes the prefix.
// Note that the name strings must still be null terminated even at the ntdll level.
if (maxLength == 0)
maxLength = (64 * 1024);
#elif CARB_POSIX
const char prefix[] = "/";
if (maxLength == 0)
{
# if CARB_PLATFORM_MACOS
// Mac OS limits the SHM name to this.
maxLength = PSHMNAMLEN;
# else
// This appears to be the specified length in POSIX.
maxLength = NAME_MAX;
# endif
}
#else
CARB_UNSUPPORTED_PLATFORM();
#endif
fname = std::string(prefix) + name;
if (fname.length() > maxLength)
{
fname.erase(fname.begin() + maxLength, fname.end());
}
return fname;
}
std::string makeUniqueName(const char* name)
{
std::string str = name;
char buffer[256];
// create a unique name be appending the process ID and a random number to the given name.
// This should be sufficiently unique for our purposes. This should only add 3-8 new
// characters to the name.
extras::formatString(buffer, CARB_COUNTOF(buffer), "%" OMNI_PRIxpid "-%x", this_process::getId(), rand());
return std::string(str + buffer);
}
static constexpr uint32_t getAccessModeFlags(AccessMode access, FlagType type)
{
switch (access)
{
default:
case AccessMode::eDefault:
case AccessMode::eReadWrite:
#if CARB_PLATFORM_WINDOWS
return type == FlagType::eFileFlags ? CARBWIN_FILE_MAP_ALL_ACCESS : CARBWIN_PAGE_READWRITE;
#elif CARB_POSIX
return type == FlagType::eFileFlags ? O_RDWR : (PROT_READ | PROT_WRITE);
#else
CARB_UNSUPPORTED_PLATFORM();
#endif
case AccessMode::eReadOnly:
#if CARB_PLATFORM_WINDOWS
return type == FlagType::eFileFlags ? CARBWIN_FILE_MAP_READ : CARBWIN_PAGE_READONLY;
#elif CARB_POSIX
return type == FlagType::eFileFlags ? O_RDONLY : PROT_READ;
#else
CARB_UNSUPPORTED_PLATFORM();
#endif
}
}
Result createAndOrOpen(const char* name, size_t size, uint32_t flags, bool tryOpen, bool tryCreate)
{
std::string mappingName;
std::string rawName;
size_t extraSize = 0;
OpenTokenImpl* token;
SharedHandle handle;
bool quiet = !!(flags & fQuiet);
/****** check for bad calls and bad parameters ******/
if (m_token != nullptr)
{
CARB_LOG_WARN(
"the previous SHM region has not been closed yet. Please close it before creating a new SHM region.");
return eError;
}
// a valid name is needed => fail.
if (name == nullptr || name[0] == 0)
return eError;
// can't create a zero-sized SHM region => fail.
if (size == 0)
return eError;
// neither create nor open => fail.
if (!tryOpen && !tryCreate)
return eError;
/****** create the named mapping object ******/
bool const unique = (flags & fCreateMakeUnique) != 0;
if (unique)
rawName = makeUniqueName(name);
else
rawName = name;
// get the platform-specific name for the region using the given name as a template.
mappingName = getPlatformMappingName(rawName.c_str());
// make sure the mapping size is aligned to the next system page size.
size = alignPageCeiling(size);
// create the open token that will be used for other clients.
extraSize = rawName.length();
token = reinterpret_cast<OpenTokenImpl*>(malloc(sizeof(OpenTokenImpl) + extraSize + 1));
if (token == nullptr)
{
if (!quiet)
CARB_LOG_ERROR("failed to create a new open token for the SHM region '%s'.", name);
return eError;
}
// store the token information.
token->size = size;
token->nameLength = extraSize & 0xffff;
memcpy(token->name, rawName.c_str(), sizeof(name[0]) * (token->nameLength + 1));
#if CARB_PLATFORM_WINDOWS
std::wstring fname = carb::extras::convertUtf8ToWide(mappingName.c_str());
if (!tryCreate)
handle.handleWin32 = OpenFileMappingW(CARBWIN_PAGE_READWRITE, CARBWIN_FALSE, fname.c_str());
else
handle.handleWin32 =
CreateFileMappingW(CARBWIN_INVALID_HANDLE_VALUE, nullptr, CARBWIN_PAGE_READWRITE,
static_cast<DWORD>(size >> 32), static_cast<DWORD>(size), fname.c_str());
// the handle was opened successfully => make sure it didn't open an existing object.
if (handle.handleWin32 == nullptr || (!tryOpen && (GetLastError() == CARBWIN_ERROR_ALREADY_EXISTS)))
{
if (!quiet)
CARB_LOG_ERROR("failed to create and/or open a file mapping object with the name '%s' {error = %" PRIu32
"}",
name, GetLastError());
CloseHandle(handle.handleWin32);
free(token);
return eError;
}
bool const wasOpened = (GetLastError() == CARBWIN_ERROR_ALREADY_EXISTS);
if (wasOpened)
{
// We need to use an undocumented function (NtQuerySection) to read the size of the mapping object.
using PNtQuerySection = DWORD(__stdcall*)(HANDLE, int, PVOID, ULONG, PSIZE_T);
static PNtQuerySection pNtQuerySection =
(PNtQuerySection)::GetProcAddress(::GetModuleHandleW(L"ntdll.dll"), "NtQuerySection");
if (pNtQuerySection)
{
struct /*SECTION_BASIC_INFORMATION*/
{
PVOID BaseAddress;
ULONG AllocationAttributes;
CARBWIN_LARGE_INTEGER MaximumSize;
} sbi;
SIZE_T read;
if (pNtQuerySection(handle.handleWin32, 0 /*SectionBasicInformation*/, &sbi, sizeof(sbi), &read) >= 0)
{
if (size > (size_t)sbi.MaximumSize.QuadPart)
{
if (!quiet)
CARB_LOG_ERROR("mapping with name '%s' was opened but existing size %" PRId64
" is smaller than requested size %zu",
name, sbi.MaximumSize.QuadPart, size);
CloseHandle(handle.handleWin32);
free(token);
return eError;
}
}
}
}
#elif CARB_POSIX
// See the function for an explanation of why this is needed.
detail::probeSharedMemory();
// Lock a mutex (named semaphore) while we attempt to initialize the ref-count and shared memory objects. For
// uniquely-named objects we use a per-process semaphore, but for globally-named objects we use a global mutex.
cpp::optional<detail::NamedSemaphore> processMutex;
cpp::optional<std::lock_guard<detail::NamedSemaphore>> lock;
if (!(flags & fNoMutexLock))
{
if (unique)
{
// Always get the current process ID since we could fork() and our process ID could change.
// NOTE: Do not change this naming. Though PID is not a great unique identifier, it is considered an
// ABI break to change this.
std::string name = detail::getGlobalSemaphoreName();
name += '-';
name += std::to_string(this_process::getId());
processMutex.emplace(name.c_str(), true);
lock.emplace(processMutex.value());
}
else
{
lock.emplace(m_systemMutex);
}
}
// create the reference count object. Note that we don't make sure it's unique in the
// system because another process may have crashed or leaked it.
if (!tryCreate || !initRefCount(token->name, O_CREAT | O_EXCL, !tryOpen && !quiet))
{
// Couldn't create it exclusively. Something else must have created it, so just try to open existing.
if (!tryOpen || !initRefCount(token->name, 0, !quiet))
{
if (!quiet)
CARB_LOG_ERROR(
"failed to create/open the reference count object for the new region with the name '%s'.",
token->name);
free(token);
return eError;
}
}
handle.handleFd =
tryCreate ? shm_open(mappingName.c_str(), O_RDWR | O_CREAT | O_EXCL, detail::kAllReadWrite) : -1;
if (handle.handleFd != -1)
{
// We created the shared memory region. Since shm_open() is affected by the process umask, use fchmod() to
// set the file permissions to all users.
fchmod(handle.handleFd, detail::kAllReadWrite);
}
// failed to open the SHM region => fail.
bool wasOpened = false;
if (handle.handleFd == -1)
{
// Couldn't create exclusively. Perhaps it already exists to open.
if (tryOpen)
{
handle.handleFd = shm_open(mappingName.c_str(), O_RDWR, 0);
}
if (handle.handleFd == -1)
{
if (!quiet)
CARB_LOG_ERROR("failed to create/open SHM region '%s' {errno = %d/%s}", name, errno, strerror(errno));
destroyRefCount(token->name);
free(token);
return eError;
}
wasOpened = true;
// If the region is too small, extend it while we have the semaphore locked.
struct stat statbuf;
if (fstat(handle.handleFd, &statbuf) == -1)
{
if (!quiet)
CARB_LOG_ERROR("failed to stat SHM region '%s' {errno = %d, %s}", name, errno, strerror(errno));
::close(handle.handleFd);
free(token);
return eError;
}
if (size > size_t(statbuf.st_size) && ftruncate(handle.handleFd, size) != 0)
{
if (!quiet)
CARB_LOG_ERROR("failed to grow the size of the SHM region '%s' from %zu to %zu bytes {errno = %d/%s}",
name, size_t(statbuf.st_size), size, errno, strerror(errno));
::close(handle.handleFd);
free(token);
return eError;
}
}
// set the size of the region by truncating the file while we have the semaphore locked.
else if (ftruncate(handle.handleFd, size) != 0)
{
if (!quiet)
CARB_LOG_ERROR("failed to set the size of the SHM region '%s' to %zu bytes {errno = %d/%s}", name, size,
errno, strerror(errno));
::close(handle.handleFd);
shm_unlink(mappingName.c_str());
destroyRefCount(token->name);
free(token);
return eError;
}
#else
CARB_UNSUPPORTED_PLATFORM();
#endif
/****** save the values for the SHM region ******/
m_token = token;
m_handle = handle;
m_access = AccessMode::eReadWrite;
return wasOpened ? eOpened : eCreated;
}
#if CARB_POSIX
bool initRefCount(const char* name, int flags, bool logError)
{
// build the name for the semaphore. This needs to start with a slash followed by up to
// 250 non-slash ASCII characters. This is the same format as for the SHM region, except
// for the maximum length. The name given here will need to be truncated if it is very
// long. The system adds "sem." to the name internally which explains why the limit is
// not NAME_MAX.
std::string mappingName = getPlatformMappingName(name, NAME_MAX - 4);
// create the semaphore that will act as the IPC reference count for the SHM region.
// Note that this will be created with an initial count of 0, not 1. This is intentional
// since we want the last reference to fail the wait operation so that we can atomically
// detect the case where the region's file (and the semaphore's) should be unlinked.
m_refCount = sem_open(mappingName.c_str(), flags, detail::kAllReadWrite, 0);
if (m_refCount == SEM_FAILED)
{
if (logError)
{
CARB_LOG_ERROR("failed to create or open a semaphore named \"%s\" {errno = %d/%s}", mappingName.c_str(),
errno, strerror(errno));
}
return false;
}
# if CARB_PLATFORM_LINUX
else if (flags & O_CREAT)
{
// sem_open() is masked by umask(), so force the permissions with chmod().
// NOTE: This assumes that named semaphores are under /dev/shm and are prefixed with sem. This is not ideal,
// but there does not appear to be any means to translate a sem_t* to a file descriptor (for fchmod()) or a
// path.
mappingName.replace(0, 1, "/dev/shm/sem.");
chmod(mappingName.c_str(), detail::kAllReadWrite);
}
# endif
// only add a new reference to the SHM region when opening the region, not when creating
// it. This will cause the last reference to fail the wait in releaseRef() to allow us
// to atomically detect the destruction case.
if ((flags & O_CREAT) == 0)
CARB_RETRY_EINTR(sem_post(m_refCount));
return true;
}
bool releaseRef()
{
int result;
// waiting on the semaphore will decrement its count by one. When it reaches zero, the
// wait will block. However since we're doing a 'try wait' here, that will fail with
// errno set to EAGAIN instead of blocking.
errno = -1;
result = CARB_RETRY_EINTR(sem_trywait(m_refCount));
return (result == -1 && errno == EAGAIN);
}
void destroyRefCount(const char* name)
{
std::string mappingName;
mappingName = getPlatformMappingName(name, NAME_MAX - 4);
sem_unlink(mappingName.c_str());
}
sem_t* m_refCount;
detail::NamedSemaphore m_systemMutex{ detail::getGlobalSemaphoreName() };
#endif
OpenTokenImpl* m_token;
SharedHandle m_handle;
AccessMode m_access;
size_t m_pageSize;
size_t m_allocationGranularity;
};
} // namespace extras
} // namespace carb