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