Add native system semaphore and Windows shared memory (#7212)

This commit is contained in:
Dominic Clark
2024-04-20 23:21:29 +01:00
committed by GitHub
parent df11a98902
commit bda042e1eb
8 changed files with 347 additions and 118 deletions

View File

@@ -218,7 +218,6 @@ jobs:
name: msvc-${{ matrix.arch }}
runs-on: windows-2019
env:
qt-version: '5.15.2'
CCACHE_MAXSIZE: 0
CCACHE_NOCOMPRESS: 1
steps:
@@ -246,22 +245,21 @@ jobs:
path: ~\AppData\Local\ccache
- name: Install tools
run: choco install ccache
- name: Install 64-bit Qt
if: matrix.arch == 'x64'
- name: Install Qt
uses: jurplel/install-qt-action@b3ea5275e37b734d027040e2c7fe7a10ea2ef946
with:
version: ${{ env.qt-version }}
arch: win64_msvc2019_64
version: '5.15.2'
arch: |-
${{
fromJSON('
{
"x86": "win32_msvc2019",
"x64": "win64_msvc2019_64"
}
')[matrix.arch]
}}
archives: qtbase qtsvg qttools
cache: true
- name: Install 32-bit Qt
uses: jurplel/install-qt-action@b3ea5275e37b734d027040e2c7fe7a10ea2ef946
with:
version: ${{ env.qt-version }}
arch: win32_msvc2019
archives: qtbase qtsvg qttools
cache: true
set-env: ${{ matrix.arch == 'x86' }}
- name: Set up build environment
uses: ilammy/msvc-dev-cmd@cec98b9d092141f74527d0afa6feb2af698cfe89
with:

View File

@@ -41,10 +41,6 @@
#ifdef LMMS_HAVE_PROCESS_H
#include <process.h>
#endif
#include <QtGlobal>
#include <QSystemSemaphore>
#include <QUuid>
#else // !(LMMS_HAVE_SYS_IPC_H && LMMS_HAVE_SEMAPHORE_H)
#ifdef LMMS_HAVE_UNISTD_H
#include <unistd.h>
@@ -75,6 +71,7 @@
#include <QProcess>
#include <QThread>
#include <QString>
#include <QUuid>
#ifndef SYNC_WITH_SHM_FIFO
#include <poll.h>
@@ -85,6 +82,7 @@
#ifdef SYNC_WITH_SHM_FIFO
#include "SharedMemory.h"
#include "SystemSemaphore.h"
#endif
namespace lmms
@@ -120,12 +118,11 @@ class shmFifo
} ;
public:
#ifndef BUILD_REMOTE_PLUGIN_CLIENT
// constructor for master-side
shmFifo() :
m_invalid( false ),
m_master( true ),
m_dataSem( QString() ),
m_messageSem( QString() ),
m_lockDepth( 0 )
{
m_data.create(QUuid::createUuid().toString().toStdString());
@@ -133,26 +130,21 @@ public:
static int k = 0;
m_data->dataSem.semKey = ( getpid()<<10 ) + ++k;
m_data->messageSem.semKey = ( getpid()<<10 ) + ++k;
m_dataSem.setKey( QString::number( m_data->dataSem.semKey ),
1, QSystemSemaphore::Create );
m_messageSem.setKey( QString::number(
m_data->messageSem.semKey ),
0, QSystemSemaphore::Create );
m_dataSem = SystemSemaphore{std::to_string(m_data->dataSem.semKey), 1u};
m_messageSem = SystemSemaphore{std::to_string(m_data->messageSem.semKey), 0u};
}
#endif
// constructor for remote-/client-side - use _shm_key for making up
// the connection to master
shmFifo(const std::string& shmKey) :
m_invalid( false ),
m_master( false ),
m_dataSem( QString() ),
m_messageSem( QString() ),
m_lockDepth( 0 )
{
m_data.attach(shmKey);
m_dataSem.setKey( QString::number( m_data->dataSem.semKey ) );
m_messageSem.setKey( QString::number(
m_data->messageSem.semKey ) );
m_dataSem = SystemSemaphore{std::to_string(m_data->dataSem.semKey)};
m_messageSem = SystemSemaphore{std::to_string(m_data->messageSem.semKey)};
}
inline bool isInvalid() const
@@ -336,11 +328,10 @@ private:
volatile bool m_invalid;
bool m_master;
SharedMemory<shmData> m_data;
QSystemSemaphore m_dataSem;
QSystemSemaphore m_messageSem;
SystemSemaphore m_dataSem;
SystemSemaphore m_messageSem;
std::atomic_int m_lockDepth;
} ;
};
#endif // SYNC_WITH_SHM_FIFO

61
include/SystemSemaphore.h Normal file
View File

@@ -0,0 +1,61 @@
/*
* SystemSemaphore.h
*
* Copyright (c) 2024 Dominic Clark
*
* This file is part of LMMS - https://lmms.io
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program (see COPYING); if not, write to the
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301 USA.
*/
#ifndef LMMS_SYSTEM_SEMAPHORE_H
#define LMMS_SYSTEM_SEMAPHORE_H
#include <memory>
#include <string>
namespace lmms {
namespace detail {
class SystemSemaphoreImpl;
} // namespace detail
class SystemSemaphore
{
public:
SystemSemaphore() noexcept;
SystemSemaphore(std::string key, unsigned int value);
explicit SystemSemaphore(std::string key);
~SystemSemaphore();
SystemSemaphore(SystemSemaphore&& other) noexcept;
auto operator=(SystemSemaphore&& other) noexcept -> SystemSemaphore&;
auto acquire() noexcept -> bool;
auto release() noexcept -> bool;
auto key() const noexcept -> const std::string& { return m_key; }
private:
std::string m_key;
std::unique_ptr<detail::SystemSemaphoreImpl> m_impl;
};
} // namespace lmms
#endif // LMMS_SYSTEM_SEMAPHORE_H

View File

@@ -22,6 +22,9 @@ FOREACH( OUTPUTCONFIG ${CMAKE_CONFIGURATION_TYPES} )
SET("CMAKE_RUNTIME_OUTPUT_DIRECTORY_${OUTPUTCONFIG}" "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}")
ENDFOREACH()
# Import of windows.h breaks min()/max()
add_definitions(-DNOMINMAX)
ADD_SUBDIRECTORY("${LMMS_SOURCE_DIR}/src/common" common)
if(NOT IS_WIN)
@@ -64,12 +67,6 @@ if(MSVC)
)
endif()
if(WIN32)
find_package(Qt5Core REQUIRED)
target_link_libraries(${EXE_NAME} Qt5::Core)
endif()
if(IS_MINGW)
SET(CMAKE_REQUIRED_FLAGS "-std=c++17")
@@ -96,7 +93,9 @@ if(LMMS_BUILD_WIN32)
set(NOOP_COMMAND "${CMAKE_COMMAND}" "-E" "echo")
endif()
if(STRIP)
set(STRIP_COMMAND "$<IF:$<TARGET:Debug,RelWithDebInfo>,${NOOP_COMMAND},${STRIP}>")
# TODO CMake 3.19: Now that CONFIG generator expressions support testing for
# multiple configurations, combine the OR into a single CONFIG expression.
set(STRIP_COMMAND "$<IF:$<OR:$<CONFIG:Debug>,$<CONFIG:RelWithDebInfo>>,${NOOP_COMMAND},${STRIP}>")
else()
set(STRIP_COMMAND "${NOOP_COMMAND}")
endif()

View File

@@ -17,23 +17,6 @@ ENDMACRO()
IF(LMMS_BUILD_WIN32 AND NOT LMMS_BUILD_WIN64)
ADD_SUBDIRECTORY(RemoteVstPlugin)
ELSEIF(LMMS_BUILD_WIN64 AND MSVC)
IF(NOT QT_32_PREFIX)
SET(LMMS_MSVC_YEAR_FOR_QT ${LMMS_MSVC_YEAR})
if(LMMS_MSVC_YEAR_FOR_QT EQUAL 2019 AND Qt5_VERSION VERSION_LESS "5.15")
SET(LMMS_MSVC_YEAR_FOR_QT 2017) # Qt only provides binaries for MSVC 2017, but 2019 is binary compatible
endif()
GET_FILENAME_COMPONENT(QT_BIN_DIR ${QT_QMAKE_EXECUTABLE} DIRECTORY)
SET(QT_32_PREFIX "${QT_BIN_DIR}/../../msvc${LMMS_MSVC_YEAR_FOR_QT}")
ENDIF()
#TODO: qt5 installed using vcpkg: I don't know how to detect if the user built the x86 version of qt5 from here. At least not cleanly.
#So for the moment, we'll allow the built.
IF(NOT (IS_DIRECTORY ${QT_32_PREFIX} AND EXISTS ${QT_32_PREFIX}/bin/qmake.exe))
MESSAGE(WARNING "No Qt 32 bit installation found at ${QT_32_PREFIX}. If you're using VCPKG you can ignore this message if you've built x86-windows version of qt5")
ENDIF()
ExternalProject_Add(RemoteVstPlugin32
"${EXTERNALPROJECT_ARGS}"
CMAKE_GENERATOR "${LMMS_MSVC_GENERATOR}"
@@ -42,7 +25,6 @@ ELSEIF(LMMS_BUILD_WIN64 AND MSVC)
CMAKE_ARGS
"${EXTERNALPROJECT_CMAKE_ARGS}"
"-DCMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE}"
"-DCMAKE_PREFIX_PATH=${QT_32_PREFIX}"
)
INSTALL_EXTERNAL_PROJECT(RemoteVstPlugin32)

View File

@@ -1,6 +1,7 @@
set(COMMON_SRCS
RemotePluginBase.cpp
SharedMemory.cpp
SystemSemaphore.cpp
)
foreach(SRC ${COMMON_SRCS})

View File

@@ -23,45 +23,44 @@
#include "SharedMemory.h"
#include <system_error>
#include <utility>
#include "lmmsconfig.h"
#include "RaiiHelpers.h"
#ifdef LMMS_HAVE_UNISTD_H
# include <unistd.h>
#endif
#if _POSIX_SHARED_MEMORY_OBJECTS > 0
# include <system_error>
#
#if _POSIX_SHARED_MEMORY_OBJECTS > 0 || defined(LMMS_BUILD_APPLE)
# include <sys/mman.h>
# include <sys/stat.h>
# include <fcntl.h>
#
# include "RaiiHelpers.h"
#elif defined(LMMS_BUILD_WIN32)
# include <windows.h>
#else
# include <stdexcept>
#
# include <QtGlobal>
# include <QSharedMemory>
# error "No shared memory implementation available"
#endif
namespace lmms::detail {
#if _POSIX_SHARED_MEMORY_OBJECTS > 0
#if _POSIX_SHARED_MEMORY_OBJECTS > 0 || defined(LMMS_BUILD_APPLE)
namespace {
[[noreturn]] void throwSystemError(const char* message)
{
throw std::system_error{errno, std::generic_category(), message};
}
template<typename F>
int retryWhileInterrupted(F&& function) noexcept(std::is_nothrow_invocable_v<F>)
{
int result;
do
{
result = function();
while (true) {
const auto result = function();
if (result != -1 || errno != EINTR) { return result; }
}
while (result == -1 && errno == EINTR);
return result;
}
void deleteFileDescriptor(int fd) noexcept { retryWhileInterrupted([fd]() noexcept { return close(fd); }); }
@@ -82,22 +81,15 @@ public:
const auto fd = FileDescriptor{
retryWhileInterrupted([&]() noexcept { return shm_open(m_key.c_str(), openFlags, 0); })
};
if (!fd)
{
throw std::system_error{errno, std::generic_category(), "SharedMemoryImpl: shm_open() failed"};
}
if (!fd) { throwSystemError("SharedMemoryImpl: shm_open() failed"); }
auto stat = (struct stat){};
if (fstat(fd.get(), &stat) == -1)
{
throw std::system_error{errno, std::generic_category(), "SharedMemoryImpl: fstat() failed"};
}
if (fstat(fd.get(), &stat) == -1) { throwSystemError("SharedMemoryImpl: fstat() failed"); }
m_size = stat.st_size;
const auto mappingProtection = readOnly ? PROT_READ : PROT_READ | PROT_WRITE;
m_mapping = mmap(nullptr, m_size, mappingProtection, MAP_SHARED, fd.get(), 0);
if (m_mapping == MAP_FAILED)
{
throw std::system_error{errno, std::generic_category(), "SharedMemoryImpl: mmap() failed"};
}
if (m_mapping == MAP_FAILED) { throwSystemError("SharedMemoryImpl: mmap() failed"); }
}
SharedMemoryImpl(const std::string& key, std::size_t size, bool readOnly) :
@@ -107,21 +99,16 @@ public:
const auto fd = FileDescriptor{
retryWhileInterrupted([&]() noexcept { return shm_open(m_key.c_str(), O_RDWR | O_CREAT | O_EXCL, 0600); })
};
if (fd.get() == -1)
{
throw std::system_error{errno, std::generic_category(), "SharedMemoryImpl: shm_open() failed"};
}
if (fd.get() == -1) { throwSystemError("SharedMemoryImpl: shm_open() failed"); }
m_object.reset(m_key.c_str());
if (retryWhileInterrupted([&]() noexcept { return ftruncate(fd.get(), m_size); }) == -1)
{
throw std::system_error{errno, std::generic_category(), "SharedMemoryImpl: ftruncate() failed"};
if (retryWhileInterrupted([&]() noexcept { return ftruncate(fd.get(), m_size); }) == -1) {
throwSystemError("SharedMemoryImpl: ftruncate() failed");
}
const auto mappingProtection = readOnly ? PROT_READ : PROT_READ | PROT_WRITE;
m_mapping = mmap(nullptr, m_size, mappingProtection, MAP_SHARED, fd.get(), 0);
if (m_mapping == MAP_FAILED)
{
throw std::system_error{errno, std::generic_category(), "SharedMemoryImpl: mmap() failed"};
}
if (m_mapping == MAP_FAILED) { throwSystemError("SharedMemoryImpl: mmap() failed"); }
}
SharedMemoryImpl(const SharedMemoryImpl&) = delete;
@@ -132,7 +119,7 @@ public:
munmap(m_mapping, m_size);
}
void* get() { return m_mapping; }
auto get() const noexcept -> void* { return m_mapping; }
private:
std::string m_key;
@@ -141,38 +128,67 @@ private:
ShmObject m_object;
};
#else
#elif defined(LMMS_BUILD_WIN32)
namespace {
auto sizeToHighAndLow(std::size_t size) -> std::pair<DWORD, DWORD>
{
if constexpr(sizeof(std::size_t) <= sizeof(DWORD)) {
return {0, size};
} else {
return {static_cast<DWORD>(size >> 32), static_cast<DWORD>(size)};
}
}
[[noreturn]] void throwLastError(const char* message)
{
throw std::system_error{static_cast<int>(GetLastError()), std::system_category(), message};
}
using UniqueHandle = UniqueNullableResource<HANDLE, nullptr, CloseHandle>;
using FileView = UniqueNullableResource<void*, nullptr, UnmapViewOfFile>;
} // namespace
class SharedMemoryImpl
{
public:
SharedMemoryImpl(const std::string& key, bool readOnly) :
m_shm{QString::fromStdString(key)}
SharedMemoryImpl(const std::string& key, bool readOnly)
{
const auto mode = readOnly ? QSharedMemory::ReadOnly : QSharedMemory::ReadWrite;
if (!m_shm.attach(mode))
{
throw std::runtime_error{"SharedMemoryImpl: QSharedMemory::attach() failed"};
}
const auto access = readOnly ? FILE_MAP_READ : FILE_MAP_WRITE;
m_mapping.reset(OpenFileMappingA(access, false, key.c_str()));
if (!m_mapping) { throwLastError("SharedMemoryImpl: OpenFileMappingA() failed"); }
m_view.reset(MapViewOfFile(m_mapping.get(), access, 0, 0, 0));
if (!m_view) { throwLastError("SharedMemoryImpl: MapViewOfFile() failed"); }
}
SharedMemoryImpl(const std::string& key, std::size_t size, bool readOnly) :
m_shm{QString::fromStdString(key)}
SharedMemoryImpl(const std::string& key, std::size_t size, bool readOnly)
{
const auto mode = readOnly ? QSharedMemory::ReadOnly : QSharedMemory::ReadWrite;
if (!m_shm.create(size, mode))
{
throw std::runtime_error{"SharedMemoryImpl: QSharedMemory::create() failed"};
const auto [high, low] = sizeToHighAndLow(size);
m_mapping.reset(CreateFileMappingA(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, high, low, key.c_str()));
// This constructor is supposed to create a new shared memory object,
// but passing the name of an existing object causes CreateFileMappingA
// to succeed and return a handle to that object. Thus we have to check
// GetLastError() too.
if (!m_mapping || GetLastError() == ERROR_ALREADY_EXISTS) {
throwLastError("SharedMemoryImpl: CreateFileMappingA() failed");
}
const auto access = readOnly ? FILE_MAP_READ : FILE_MAP_WRITE;
m_view.reset(MapViewOfFile(m_mapping.get(), access, 0, 0, 0));
if (!m_view) { throwLastError("SharedMemoryImpl: MapViewOfFile() failed"); }
}
SharedMemoryImpl(const SharedMemoryImpl&) = delete;
SharedMemoryImpl& operator=(const SharedMemoryImpl&) = delete;
auto operator=(const SharedMemoryImpl&) -> SharedMemoryImpl& = delete;
void* get() { return m_shm.data(); }
auto get() const noexcept -> void* { return m_view.get(); }
private:
QSharedMemory m_shm;
UniqueHandle m_mapping;
FileView m_view;
};
#endif
@@ -196,7 +212,7 @@ SharedMemoryData::~SharedMemoryData() = default;
SharedMemoryData::SharedMemoryData(SharedMemoryData&& other) noexcept :
m_key{std::move(other.m_key)},
m_impl{std::move(other.m_impl)},
m_ptr{other.m_ptr}
m_ptr{std::exchange(other.m_ptr, nullptr)}
{ }
} // namespace lmms::detail

View File

@@ -0,0 +1,181 @@
/*
* SystemSemaphore.cpp
*
* Copyright (c) 2024 Dominic Clark
*
* This file is part of LMMS - https://lmms.io
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program (see COPYING); if not, write to the
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301 USA.
*/
#include "SystemSemaphore.h"
#include <limits>
#include <system_error>
#include <type_traits>
#include <utility>
#include "lmmsconfig.h"
#include "RaiiHelpers.h"
#ifdef LMMS_HAVE_UNISTD_H
# include <unistd.h>
#endif
#if (_POSIX_SEMAPHORES > 0 && !defined(__MINGW32__)) || defined(LMMS_BUILD_APPLE)
# include <fcntl.h>
# include <semaphore.h>
#elif defined(LMMS_BUILD_WIN32)
# include <windows.h>
#else
# error "No system semaphore implementation available"
#endif
namespace lmms {
namespace detail {
#if (_POSIX_SEMAPHORES > 0 && !defined(__MINGW32__)) || defined(LMMS_BUILD_APPLE)
namespace {
[[noreturn]] void throwSystemError(const char* message)
{
throw std::system_error{errno, std::generic_category(), message};
}
template<typename F>
auto retryWhileInterrupted(F&& function, std::invoke_result_t<F> error = -1)
noexcept(std::is_nothrow_invocable_v<F>) -> auto
{
while (true)
{
const auto result = function();
if (result != error || errno != EINTR) { return result; }
}
}
using UniqueSemaphore = UniqueNullableResource<const char*, nullptr, sem_unlink>;
} // namespace
class SystemSemaphoreImpl
{
public:
SystemSemaphoreImpl(const std::string& key, unsigned int value) :
m_key{"/" + key},
m_sem{retryWhileInterrupted([&]() noexcept {
return sem_open(m_key.c_str(), O_CREAT | O_EXCL, 0600, value);
}, SEM_FAILED)}
{
if (m_sem == SEM_FAILED) { throwSystemError("SystemSemaphoreImpl: sem_open() failed"); }
m_ownedSemaphore.reset(m_key.c_str());
}
explicit SystemSemaphoreImpl(const std::string& key) :
m_key{"/" + key},
m_sem{retryWhileInterrupted([&]() noexcept {
return sem_open(m_key.c_str(), 0);
}, SEM_FAILED)}
{
if (m_sem == SEM_FAILED) { throwSystemError("SystemSemaphoreImpl: sem_open() failed"); }
}
~SystemSemaphoreImpl()
{
// We can't use `UniqueNullableResource` for `m_sem`, as the null value
// (`SEM_FAILED`) is not a constant expression on macOS (it's defined as
// `(sem_t*) -1`), so can't be used as a template parameter.
sem_close(m_sem);
}
auto acquire() noexcept -> bool
{
return retryWhileInterrupted([&]() noexcept {
return sem_wait(m_sem);
}) == 0;
}
auto release() noexcept -> bool { return sem_post(m_sem) == 0; }
private:
std::string m_key;
sem_t* m_sem;
UniqueSemaphore m_ownedSemaphore;
};
#elif defined(LMMS_BUILD_WIN32)
namespace {
[[noreturn]] void throwSystemError(const char* message)
{
throw std::system_error{static_cast<int>(GetLastError()), std::system_category(), message};
}
using UniqueHandle = UniqueNullableResource<HANDLE, nullptr, CloseHandle>;
} // namespace
class SystemSemaphoreImpl
{
public:
SystemSemaphoreImpl(const std::string& key, unsigned int value) :
m_sem{CreateSemaphoreA(nullptr, value, std::numeric_limits<LONG>::max(), key.c_str())}
{
if (!m_sem || GetLastError() == ERROR_ALREADY_EXISTS) {
throwSystemError("SystemSemaphoreImpl: CreateSemaphoreA failed");
}
}
explicit SystemSemaphoreImpl(const std::string& key) :
m_sem{OpenSemaphoreA(SEMAPHORE_MODIFY_STATE | SYNCHRONIZE, false, key.c_str())}
{
if (!m_sem) { throwSystemError("SystemSemaphoreImpl: OpenSemaphoreA failed"); }
}
auto acquire() noexcept -> bool { return WaitForSingleObject(m_sem.get(), INFINITE) == WAIT_OBJECT_0; }
auto release() noexcept -> bool { return ReleaseSemaphore(m_sem.get(), 1, nullptr); }
private:
UniqueHandle m_sem;
};
#endif
} // namespace detail
SystemSemaphore::SystemSemaphore() noexcept = default;
SystemSemaphore::SystemSemaphore(std::string key, unsigned int value) :
m_key{std::move(key)},
m_impl{std::make_unique<detail::SystemSemaphoreImpl>(m_key, value)}
{}
SystemSemaphore::SystemSemaphore(std::string key) :
m_key{std::move(key)},
m_impl{std::make_unique<detail::SystemSemaphoreImpl>(m_key)}
{}
SystemSemaphore::~SystemSemaphore() = default;
SystemSemaphore::SystemSemaphore(SystemSemaphore&& other) noexcept = default;
auto SystemSemaphore::operator=(SystemSemaphore&& other) noexcept -> SystemSemaphore& = default;
auto SystemSemaphore::acquire() noexcept -> bool { return m_impl->acquire(); }
auto SystemSemaphore::release() noexcept -> bool { return m_impl->release(); }
} // namespace lmms