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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ target_include_directories(kickmsg

target_link_libraries(kickmsg PUBLIC ${OS_LIBRARIES})

# --- Version header (generated from git: cmake/version.cmake) ---
include(cmake/version.cmake)

# --- Install, pkg-config, find_package ---
include(cmake/install.cmake)

Expand Down
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ for frame in diag.watch("/kickmsg_telemetry", interval=1.0):
```bash
# Install dependencies
pip install conan
conan install conanfile.py -of=build --build=missing -o unit_tests=True
conan install conan/conanfile.py -of=build --build=missing -o unit_tests=True

# Configure and build
cmake -S . -B build \
Expand Down Expand Up @@ -218,6 +218,10 @@ python examples/python/hello_schema.py
python examples/python/cli_playground.py # long-running, drive the `kickmsg` CLI against it
```

To prove kickmsg works on your own hardware -- the validation ladder from a
quick `ctest` gate to a multi-hour contention soak with a single
`VERDICT: ALL CLEAN` -- see [tests/README.md](tests/README.md).

### As a subdirectory

```cmake
Expand All @@ -242,7 +246,7 @@ target_link_libraries(my_app PRIVATE kickmsg)
| macOS | `shm_open` / `mmap` | `__ulock_wait` / `__ulock_wake` |
| Windows | `CreateFileMapping` / `MapViewOfFile` | `WaitOnAddress` / `WakeByAddressAll` |

Actively validated on Linux x86-64, Linux ARM64 (Raspberry Pi 4B, 12 h continuous stress), and Darwin ARM64 (Apple Silicon) via `scripts/validate.sh`.
Actively validated on Linux x86-64, Linux ARM64 (Raspberry Pi 4B, 12 h continuous stress), and Darwin ARM64 (Apple Silicon, 12 h continuous stress: 2660 passes, 0 failures, 0 reorders) via `scripts/validate.sh` and `tests/endurance.sh`.

## Architecture

Expand Down
7 changes: 6 additions & 1 deletion cmake/install.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ include(CMakePackageConfigHelpers)
install(DIRECTORY include/kickmsg
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)
# version.h is generated (cmake/version.cmake), not committed -- install it
# alongside the committed headers.
install(FILES ${KICKMSG_VERSION_H}
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/kickmsg
)

# --- Library target + export ---
set_target_properties(kickmsg PROPERTIES EXPORT_NAME kickmsg)
Expand Down Expand Up @@ -40,7 +45,7 @@ configure_package_config_file(

write_basic_package_version_file(
${CMAKE_CURRENT_BINARY_DIR}/kickmsgConfigVersion.cmake
VERSION 1.0.0
VERSION ${KICKMSG_VERSION}
COMPATIBILITY SameMajorVersion
)

Expand Down
2 changes: 1 addition & 1 deletion cmake/kickmsg.pc.in
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ includedir=${prefix}/@CMAKE_INSTALL_INCLUDEDIR@

Name: kickmsg
Description: Lock-free shared-memory MPMC messaging library
Version: 1.0.0
Version: @KICKMSG_VERSION@
Libs: -L${libdir} @KICKMSG_PC_LIBS@ @KICKMSG_PC_SYSLIBS@
Cflags: -I${includedir}
112 changes: 112 additions & 0 deletions cmake/version.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# Generate <build>/generated/kickmsg/version.h from cmake/version.h.in, with the
# version + git metadata derived from `git describe`. The header is regenerated
# on every build so the stamp tracks the working tree (commits, dirty state).
#
# Two modes:
# - included from CMakeLists (configure time): generates the header now and
# wires a build-time regeneration target onto the kickmsg library.
# - cmake -D KICKMSG_VERSION_SRC=<src> -D KICKMSG_VERSION_OUT=<file> -P version.cmake
# (build time): re-runs git and rewrites the header.
#
# With no git available (source tarball / conan-from-recipe) it falls back to
# 0.0.0 and "unknown" git fields.

function(_kickmsg_compute_version src_dir)
set(_describe "unknown")
set(_branch "unknown")
set(_tag "")
set(_hash "unknown")
set(_dirty 0)
set(_ver "0.0.0")

if(EXISTS "${src_dir}/.git")
execute_process(COMMAND git describe --always --tags --dirty
WORKING_DIRECTORY "${src_dir}" OUTPUT_VARIABLE _describe
OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET)
execute_process(COMMAND git rev-parse --abbrev-ref HEAD
WORKING_DIRECTORY "${src_dir}" OUTPUT_VARIABLE _branch
OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET)
execute_process(COMMAND git describe --tags --exact-match
WORKING_DIRECTORY "${src_dir}" OUTPUT_VARIABLE _tag
OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET)
execute_process(COMMAND git rev-parse --short HEAD
WORKING_DIRECTORY "${src_dir}" OUTPUT_VARIABLE _hash
OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET)
execute_process(COMMAND git status --porcelain
WORKING_DIRECTORY "${src_dir}" OUTPUT_VARIABLE _status
OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET)
if(_status)
set(_dirty 1)
endif()
# Nearest tag -> clean MAJOR.MINOR.PATCH for packaging/version string.
execute_process(COMMAND git describe --tags --abbrev=0
WORKING_DIRECTORY "${src_dir}" OUTPUT_VARIABLE _nearest
OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET)
string(REGEX REPLACE "^v" "" _nearest "${_nearest}")
if(_nearest MATCHES "^[0-9]+\\.[0-9]+\\.[0-9]+")
set(_ver "${_nearest}")
endif()
endif()

if(_ver MATCHES "^([0-9]+)\\.([0-9]+)\\.([0-9]+)")
set(_maj "${CMAKE_MATCH_1}")
set(_min "${CMAKE_MATCH_2}")
set(_pat "${CMAKE_MATCH_3}")
else()
set(_maj 0)
set(_min 0)
set(_pat 0)
endif()

set(KICKMSG_VERSION_STRING "${_ver}" PARENT_SCOPE)
set(KICKMSG_VERSION_MAJOR "${_maj}" PARENT_SCOPE)
set(KICKMSG_VERSION_MINOR "${_min}" PARENT_SCOPE)
set(KICKMSG_VERSION_PATCH "${_pat}" PARENT_SCOPE)
set(KICKMSG_VERSION "${_maj}.${_min}.${_pat}" PARENT_SCOPE)
set(KICKMSG_GIT_DESCRIBE "${_describe}" PARENT_SCOPE)
set(KICKMSG_GIT_BRANCH "${_branch}" PARENT_SCOPE)
set(KICKMSG_GIT_TAG "${_tag}" PARENT_SCOPE)
set(KICKMSG_GIT_COMMIT_HASH "${_hash}" PARENT_SCOPE)
set(KICKMSG_GIT_DIRTY "${_dirty}" PARENT_SCOPE)
endfunction()

# --- Build-time mode (cmake -P) ---
if(KICKMSG_VERSION_SRC)
_kickmsg_compute_version("${KICKMSG_VERSION_SRC}")
configure_file("${KICKMSG_VERSION_SRC}/cmake/version.h.in"
"${KICKMSG_VERSION_OUT}" @ONLY)
return()
endif()

# --- Configure-time mode (include() from CMakeLists, after project()) ---
_kickmsg_compute_version("${CMAKE_CURRENT_SOURCE_DIR}")

set(KICKMSG_VERSION_H "${CMAKE_BINARY_DIR}/generated/kickmsg/version.h")
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/cmake/version.h.in"
"${KICKMSG_VERSION_H}" @ONLY)

set(_version_deps "${CMAKE_CURRENT_SOURCE_DIR}/cmake/version.h.in")
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/.git/HEAD")
list(APPEND _version_deps "${CMAKE_CURRENT_SOURCE_DIR}/.git/HEAD")
endif()
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/.git/index")
list(APPEND _version_deps "${CMAKE_CURRENT_SOURCE_DIR}/.git/index")
endif()

add_custom_command(
OUTPUT "${KICKMSG_VERSION_H}"
COMMAND ${CMAKE_COMMAND}
-D KICKMSG_VERSION_SRC=${CMAKE_CURRENT_SOURCE_DIR}
-D KICKMSG_VERSION_OUT=${KICKMSG_VERSION_H}
-P "${CMAKE_CURRENT_SOURCE_DIR}/cmake/version.cmake"
DEPENDS ${_version_deps}
COMMENT "Regenerating kickmsg/version.h from git"
VERBATIM
)
add_custom_target(kickmsg_version DEPENDS "${KICKMSG_VERSION_H}")

# Expose the generated header on the library's public include path and make
# sure it is generated before anything that includes it compiles.
target_include_directories(kickmsg PUBLIC
$<BUILD_INTERFACE:${CMAKE_BINARY_DIR}/generated>)
add_dependencies(kickmsg kickmsg_version)
21 changes: 21 additions & 0 deletions cmake/version.h.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#ifndef KICKMSG_VERSION_H
#define KICKMSG_VERSION_H

// Generated at build time from cmake/version.h.in by cmake/version.cmake.
// Do not edit. Values are derived from `git describe`; a build without git
// (e.g. a source tarball) falls back to 0.0.0 / "unknown".

#define KICKMSG_VERSION_MAJOR @KICKMSG_VERSION_MAJOR@
#define KICKMSG_VERSION_MINOR @KICKMSG_VERSION_MINOR@
#define KICKMSG_VERSION_PATCH @KICKMSG_VERSION_PATCH@

#define KICKMSG_VERSION_STRING "@KICKMSG_VERSION_STRING@"

// Precise build identity (full `git describe`, e.g. "v0.5.0-3-gabc1234-dirty").
#define KICKMSG_GIT_DESCRIBE "@KICKMSG_GIT_DESCRIBE@"
#define KICKMSG_GIT_BRANCH "@KICKMSG_GIT_BRANCH@"
#define KICKMSG_GIT_TAG "@KICKMSG_GIT_TAG@"
#define KICKMSG_GIT_COMMIT_HASH "@KICKMSG_GIT_COMMIT_HASH@"
#define KICKMSG_GIT_DIRTY @KICKMSG_GIT_DIRTY@

#endif
4 changes: 4 additions & 0 deletions conan/all/conanfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ def package(self):
dst=os.path.join(self.package_folder, "licenses"))
copy(self, "*.h", src=os.path.join(self.source_folder, "include"),
dst=os.path.join(self.package_folder, "include"))
# version.h is generated into the build tree (cmake/version.cmake),
# not committed under include/ -- package it from there.
copy(self, "version.h", src=os.path.join(self.build_folder, "generated"),
dst=os.path.join(self.package_folder, "include"))
copy(self, "*.a", src=self.build_folder,
dst=os.path.join(self.package_folder, "lib"), keep_path=False)
copy(self, "*.lib", src=self.build_folder,
Expand Down
10 changes: 0 additions & 10 deletions include/kickmsg/version.h

This file was deleted.

2 changes: 1 addition & 1 deletion scripts/validate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ info "Platform: $(uname -s) $(uname -m)"

step "Installing Conan dependencies → $BUILD_DIR"
cd "$PROJECT_DIR"
conan install conanfile.py -of="$BUILD_DIR" --build=missing -o unit_tests=True
conan install conan/conanfile.py -of="$BUILD_DIR" --build=missing -o unit_tests=True

step "Configuring CMake (Release, unit + crash tests)"
cmake -S "$PROJECT_DIR" -B "$BUILD_PATH" \
Expand Down
2 changes: 1 addition & 1 deletion src/os/darwin/SharedMemory.cc
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ namespace kickmsg
if (fd_ < 0)
{
throw std::system_error(errno, std::system_category(),
"SharedMemory: shm_open(create)");
"SharedMemory: shm_open(create) '" + name + "'");
}
map(size);
}
Expand Down
2 changes: 1 addition & 1 deletion src/os/linux/SharedMemory.cc
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ namespace kickmsg
if (fd_ < 0)
{
throw std::system_error(errno, std::system_category(),
"SharedMemory: shm_open(create)");
"SharedMemory: shm_open(create) '" + name + "'");
}
map(size);
}
Expand Down
18 changes: 13 additions & 5 deletions src/os/posix/SharedMemory.cc
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,17 @@ namespace kickmsg
{
namespace
{
[[noreturn]] void throw_system_error(char const* context)
[[noreturn]] void throw_system_error(char const* context,
std::string const& name = "")
{
throw std::system_error(errno, std::system_category(), context);
// Append the shm name so the failure points at the offending
// region rather than just naming the syscall.
std::string msg = context;
if (not name.empty())
{
msg += " '" + name + "'";
}
throw std::system_error(errno, std::system_category(), msg);
}
}

Expand Down Expand Up @@ -85,7 +93,7 @@ namespace kickmsg
fd_ = INVALID_SHM_HANDLE;
return false;
}
throw_system_error("SharedMemory: shm_open(try_create)");
throw_system_error("SharedMemory: shm_open(try_create)", name);
}
map(size);
return true;
Expand All @@ -95,7 +103,7 @@ namespace kickmsg
{
if (not try_open(name))
{
throw_system_error("SharedMemory: shm_open(open)");
throw_system_error("SharedMemory: shm_open(open)", name);
}
}

Expand All @@ -109,7 +117,7 @@ namespace kickmsg
fd_ = INVALID_SHM_HANDLE;
return false;
}
throw_system_error("SharedMemory: shm_open(try_open)");
throw_system_error("SharedMemory: shm_open(try_open)", name);
}

struct stat st{};
Expand Down
20 changes: 14 additions & 6 deletions src/os/windows/SharedMemory.cc
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
#include "kickmsg/os/SharedMemory.h"

#include <string>
#include <system_error>

namespace kickmsg
{
static void throw_last_error(char const* context)
static void throw_last_error(char const* context, std::string const& name = "")
{
// Append the mapping name so the failure points at the offending
// region rather than just naming the API call.
std::string msg = context;
if (not name.empty())
{
msg += " '" + name + "'";
}
throw std::system_error(
static_cast<int>(GetLastError()), std::system_category(), context);
static_cast<int>(GetLastError()), std::system_category(), msg);
}

SharedMemory::SharedMemory(SharedMemory&& other) noexcept
Expand Down Expand Up @@ -68,7 +76,7 @@ namespace kickmsg
fd_ = create_file_mapping(name, size);
if (fd_ == INVALID_SHM_HANDLE)
{
throw_last_error("SharedMemory: CreateFileMappingA(create)");
throw_last_error("SharedMemory: CreateFileMappingA(create)", name);
}
map(size);
}
Expand All @@ -78,7 +86,7 @@ namespace kickmsg
fd_ = create_file_mapping(name, size);
if (fd_ == INVALID_SHM_HANDLE)
{
throw_last_error("SharedMemory: CreateFileMappingA(try_create)");
throw_last_error("SharedMemory: CreateFileMappingA(try_create)", name);
}
if (GetLastError() == ERROR_ALREADY_EXISTS)
{
Expand All @@ -94,7 +102,7 @@ namespace kickmsg
{
if (not try_open(name))
{
throw_last_error("SharedMemory: OpenFileMappingA(open)");
throw_last_error("SharedMemory: OpenFileMappingA(open)", name);
}
}

Expand All @@ -107,7 +115,7 @@ namespace kickmsg
{
return false;
}
throw_last_error("SharedMemory: OpenFileMappingA(try_open)");
throw_last_error("SharedMemory: OpenFileMappingA(try_open)", name);
}

address_ = MapViewOfFile(fd_, FILE_MAP_ALL_ACCESS, 0, 0, 0);
Expand Down
Loading
Loading