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
25 changes: 25 additions & 0 deletions .github/workflows/build_and_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,28 @@ jobs:
- name: Run Tests
working-directory: build
run: ctest --build-config Release --output-on-failure --verbose

- name: Install Valgrind (Linux only)
if: matrix.os == 'ubuntu-latest'
run: sudo apt-get update && sudo apt-get install -y valgrind

- name: Reconfigure CMake with Valgrind (Linux only)
if: matrix.os == 'ubuntu-latest'
run: cmake -B build

- name: Verify Valgrind Configuration (Linux only)
if: matrix.os == 'ubuntu-latest'
working-directory: build
run: |
if ! grep -q "MemoryCheckCommand:.*valgrind" DartConfiguration.tcl; then
echo "ERROR: Valgrind was not found by CMake"
exit 1
fi
echo "Valgrind successfully configured"

- name: Run Tests with Valgrind (Linux only)
if: matrix.os == 'ubuntu-latest'
working-directory: build
run: |
ctest --build-config Release --output-on-failure --verbose -T memcheck

18 changes: 17 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,24 @@ set_target_properties(${C_API_TEST}
LINKER_LANGUAGE C
)

# Configure Valgrind for memory checks (must be BEFORE include(CTest))
find_program(VALGRIND_EXECUTABLE valgrind)
if(VALGRIND_EXECUTABLE)
set(MEMORYCHECK_COMMAND ${VALGRIND_EXECUTABLE})
set(CTEST_MEMORYCHECK_COMMAND ${VALGRIND_EXECUTABLE})

# Show only definite and possible leaks, not reachable memory (which may be held by static objects)
set(MEMORYCHECK_COMMAND_OPTIONS "--leak-check=full --show-leak-kinds=definite,possible --track-origins=yes --error-exitcode=1")
set(CTEST_MEMORYCHECK_COMMAND_OPTIONS "--leak-check=full --show-leak-kinds=definite,possible --track-origins=yes --error-exitcode=1")

if(EXISTS "${CMAKE_SOURCE_DIR}/valgrind.supp")
set(MEMORYCHECK_SUPPRESSIONS_FILE "${CMAKE_SOURCE_DIR}/valgrind.supp")
set(CTEST_MEMORYCHECK_SUPPRESSIONS_FILE "${CMAKE_SOURCE_DIR}/valgrind.supp")
endif()
endif()

# Enable CTest
enable_testing()
include(CTest)

# Add tests to CTest
add_test(NAME UnitTests COMMAND ${UNIT_TEST})
Expand Down
70 changes: 48 additions & 22 deletions include/notifly.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
#include <memory>
#include <vector>
#include <future>
#include <atomic>
#include <ranges>

// Windows.h defines min and max as macros, which conflicts with std::min and std::max
Expand All @@ -48,7 +49,7 @@
#endif

#define NOTIFLY_VERSION_MAJOR 3
#define NOTIFLY_VERSION_MINOR 4
#define NOTIFLY_VERSION_MINOR 5
#define NOTIFLY_VERSION_PATCH 0

#define NOTIFLY_VERSION (NOTIFLY_VERSION_MAJOR << 16 | NOTIFLY_VERSION_MINOR << 8 | NOTIFLY_VERSION_PATCH)
Expand Down Expand Up @@ -301,7 +302,7 @@ class notifly
notifly_result post_and_wait(
int a_post_notification,
int a_wait_notification,
int a_timeout_ms,
const int a_timeout_ms,
ResultTuple& a_result,
PostArgs... post_args)
{
Expand Down Expand Up @@ -350,6 +351,18 @@ class notifly
return instance;
}

/**
* @brief Get the number of pending async tasks (useful for testing).
*/
size_t pending_async_task_count() const
{
std::lock_guard lock(m_tasks_mutex);
size_t count = 0;
for (const auto &tasks: m_async_tasks | std::views::values)
count += tasks.size();
return count;
}

private:
/**
* @brief Helper trait to detect tuple types
Expand Down Expand Up @@ -388,16 +401,25 @@ class notifly
}

// Structure to group observer data for a notification
struct NotificationData {
struct NotificationData
{
std::list<notification_observer> observers;
};

// Structure to store observer location info for quick lookup
struct ObserverLocation {
struct ObserverLocation
{
int notification_id{};
std::list<notification_observer>::iterator iterator;
};

// Structure to track an async task and its completion status
struct AsyncTask
{
std::shared_ptr<std::jthread> thread;
std::shared_ptr<std::atomic<bool>> completed;
};

// Helper method to post a notification
template<typename ...Args>
int post_notification_impl(auto a_notification, const bool a_async, Args... args)
Expand All @@ -424,14 +446,20 @@ class notifly
{
if(a_async)
{
auto task_thread = std::make_shared<std::jthread>([callback = observer.m_callback, p = payload]
{
callback(p);
});

// Store the thread
auto completed_flag = std::make_shared<std::atomic<bool>>(false);
auto task_thread = std::make_shared<std::jthread>(
[callback = observer.m_callback, p = payload, completed_flag]()
{
callback(p);
completed_flag->store(true, std::memory_order_release);
});
std::lock_guard task_lock(m_tasks_mutex);
m_async_tasks[observer.get_id()].push_back(task_thread);
auto& tasks = m_async_tasks[observer.get_id()];
// Clean up completed tasks to prevent unbounded growth
std::erase_if(tasks, [](const AsyncTask& t) {
return t.completed->load(std::memory_order_acquire);
});
tasks.push_back({task_thread, completed_flag});
}
else
{
Expand All @@ -447,12 +475,10 @@ class notifly
std::lock_guard task_lock(m_tasks_mutex);
if (const auto it = m_async_tasks.find(observer_id); it != m_async_tasks.end())
{
for (const auto& task : it->second)
for (const auto&[thread, completed] : it->second)
{
#ifdef __APPLE__
if (task && task->joinable())
task->join();
#endif
if (thread && thread->joinable())
thread->join();
}
m_async_tasks.erase(it);
}
Expand All @@ -462,7 +488,7 @@ class notifly
void wait_for_notification_tasks(const int notification_id)
{
std::vector<int> observer_ids_to_wait;

// First gather all observer IDs for this notification
for (const auto& [id, location] : m_observer_lookup)
{
Expand All @@ -471,7 +497,7 @@ class notifly
observer_ids_to_wait.push_back(id);
}
}

// Now wait for each observer's tasks
for (const int observer_id : observer_ids_to_wait)
{
Expand All @@ -485,9 +511,10 @@ class notifly
std::lock_guard lock(m_tasks_mutex);
for (auto &tasks: m_async_tasks | std::views::values)
{
for (const auto& task : tasks)
for (const auto&[thread, completed] : tasks)
{
if (task && task->joinable()) task->join();
if (thread && thread->joinable())
thread->join();
}
}
m_async_tasks.clear();
Expand Down Expand Up @@ -539,7 +566,7 @@ class notifly
std::unordered_map<int, ObserverLocation> m_observer_lookup;

// Async tasks management
std::unordered_map<int, std::vector<std::shared_ptr<std::jthread>>> m_async_tasks;
std::unordered_map<int, std::vector<AsyncTask>> m_async_tasks;
mutable std::mutex m_tasks_mutex;

// ID management
Expand All @@ -552,4 +579,3 @@ class notifly
// Default notification center instance
static std::shared_ptr<notifly> m_default_center;
};

1 change: 1 addition & 0 deletions include/notifly_c.h
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ typedef enum {
NOTIFLY_C_API notifly_handle notifly_create(void);
NOTIFLY_C_API void notifly_destroy(notifly_handle handle);
NOTIFLY_C_API notifly_handle notifly_default(void);
NOTIFLY_C_API void notifly_cleanup_default(void); /* Cleanup default instance (for tests/shutdown) */

/* Observer management */
NOTIFLY_C_API int notifly_add_observer(notifly_handle handle, int notification_id, notifly_callback callback, void* user_data);
Expand Down
8 changes: 8 additions & 0 deletions src/notifly_c.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,14 @@ notifly_handle notifly_default(void) {
return g_default_handle;
}

void notifly_cleanup_default(void) {
std::lock_guard lock(g_default_mutex);
if (g_default_handle != nullptr) {
delete g_default_handle;
g_default_handle = nullptr;
}
}

int notifly_add_observer(notifly_handle handle, int notification_id, notifly_callback callback, void* user_data) {
if (!handle || !callback) {
return NOTIFLY_INVALID_HANDLE;
Expand Down
3 changes: 3 additions & 0 deletions test/c_api_test.c
Original file line number Diff line number Diff line change
Expand Up @@ -278,5 +278,8 @@ int main(void) {
printf(" Failed: %d\n", tests_failed);
printf("==========================================\n");

/* Cleanup default instance to prevent memory leaks */
notifly_cleanup_default();

return (tests_failed == 0) ? 0 : 1;
}
Loading