From b493d9ac70b16213383028dc36ce325dd149ec79 Mon Sep 17 00:00:00 2001 From: Philipp Kaeser Date: Mon, 24 Aug 2026 20:35:19 +0200 Subject: [PATCH 1/2] signal: Adds basic methods for pub/sub support. --- include/libbase/signal.h | 128 +++++++++++++++++++++++++++++ src/CMakeLists.txt | 2 + src/signal.c | 172 +++++++++++++++++++++++++++++++++++++++ tests/libbase_test.c | 2 + 4 files changed, 304 insertions(+) create mode 100644 include/libbase/signal.h create mode 100644 src/signal.c diff --git a/include/libbase/signal.h b/include/libbase/signal.h new file mode 100644 index 0000000..0be9efc --- /dev/null +++ b/include/libbase/signal.h @@ -0,0 +1,128 @@ +/* ========================================================================= */ +/** + * @file signal.h + * + * Support for publisher/subscriber flows, similar to Wayland's server-side + * wl_signal. Implemented separately, so it can be used w/o Wayland's server. + * + * Copyright (c) 2026 Philipp Kaeser + * + * @copyright + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#ifndef __SIGNAL_H__ +#define __SIGNAL_H__ + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +struct bs_listener; + +/** Notifier function for a listener. */ +typedef void (*bs_signal_notify_t)( + struct bs_listener *listener_ptr, + void *data_ptr); + +/** A signal (the publisher). */ +struct bs_signal { + /** Subscribed listeners, through @ref bs_listener::dlnode. */ + bs_dllist_t listeners; +}; + +/** A listener (the subscriber). */ +struct bs_listener { + /** Element of @ref bs_signal::listeners. */ + bs_dllist_node_t dlnode; + /** The notifier for this subscriber. */ + bs_signal_notify_t notify; +}; + +/** + * Emit the signal, with the provided argument. + * + * Calls each listener, with `data_ptr` as argument. It is safe to disconnect + * `listener_ptr` from `signal_ptr`, but not to modify other listeners. + * + * @param signal_ptr + * @param data_ptr + */ +void bs_signal_emit( + const struct bs_signal *signal_ptr, + void *data_ptr); + +/** + * Connects the listener with the provided notification function to the signal. + * + * @param signal_ptr + * @param listener_ptr + * @param notify + */ +void bs_signal_connect( + struct bs_signal *signal_ptr, + struct bs_listener *listener_ptr, + bs_signal_notify_t notify); + +/** + * Disconnects the listener from the signal. + * + * @param signal_ptr + * @param listener_ptr + */ +void bs_signal_disconnect( + struct bs_signal *signal_ptr, + struct bs_listener *listener_ptr); + +/** A listener that can be used in unit tests. */ +struct bs_test_listener { + /** The listener. */ + struct bs_listener listener; + /** Calls since initialization or last @ref bs_test_listener_clear. */ + size_t calls; + /** Argumet of last call, if any since @ref bs_test_listener_clear. */ + void *last_data_ptr; +}; + +/** + * Clears @ref bs_test_listener::calls and + * @ref bs_test_listener::last_data_ptr. + * + * @param test_listener_ptr + */ +void bs_test_listener_clear( + struct bs_test_listener *test_listener_ptr); + +/** Connects the test listener to `signal_ptr`. */ +void bs_test_listener_connect( + struct bs_signal *signal_ptr, + struct bs_test_listener *test_listener_ptr); + +/** Disconnects the test listener to `signal_ptr`. */ +void bs_test_listener_disconnect( + struct bs_signal *signal_ptr, + struct bs_test_listener *test_listener_ptr); + +/** Unit tests set. */ +extern const bs_test_set_t bs_signal_test_set; + +#ifdef __cplusplus +} // extern "C" +#endif // __cplusplus + +#endif // __SIGNAL_H__ +/* == End of signal.h ====================================================== */ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index d88c928..5798bce 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -31,6 +31,7 @@ set(public_header_files ptr_stack.h ptr_vector.h ref.h + signal.h sock.h strutil.h subprocess.h @@ -54,6 +55,7 @@ set(sources ptr_stack.c ptr_vector.c ref.c + signal.c sock.c strutil.c subprocess.c diff --git a/src/signal.c b/src/signal.c new file mode 100644 index 0000000..561ca1c --- /dev/null +++ b/src/signal.c @@ -0,0 +1,172 @@ +/* ========================================================================= */ +/** + * @file signal.c + * Copyright (c) 2026 Philipp Kaeser + * + * @copyright + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include +#include + +/* == Declarations ========================================================= */ + +static void _bs_signal_notify_dlnode( + bs_dllist_node_t *dlnode_ptr, + void *ud_ptr); +static void _bs_test_listener_notify( + struct bs_listener *listener_ptr, + void *data_ptr); + +/* == Exported methods ===================================================== */ + +/* ------------------------------------------------------------------------- */ +void bs_signal_emit( + const struct bs_signal *signal_ptr, + void *data_ptr) +{ + bs_dllist_for_each( + &signal_ptr->listeners, + _bs_signal_notify_dlnode, + data_ptr); +} + +/* ------------------------------------------------------------------------- */ +void bs_signal_connect( + struct bs_signal *signal_ptr, + struct bs_listener *listener_ptr, + bs_signal_notify_t notify) +{ + listener_ptr->notify = notify; + bs_dllist_push_back( + &signal_ptr->listeners, + &listener_ptr->dlnode); +} + +/* ------------------------------------------------------------------------- */ +void bs_signal_disconnect( + struct bs_signal *signal_ptr, + struct bs_listener *listener_ptr) +{ + bs_dllist_remove( + &signal_ptr->listeners, + &listener_ptr->dlnode); +} + +/* ------------------------------------------------------------------------- */ +void bs_test_listener_clear( + struct bs_test_listener *test_listener_ptr) +{ + test_listener_ptr->calls = 0; + test_listener_ptr->last_data_ptr = NULL; +} + +/* ------------------------------------------------------------------------- */ +void bs_test_listener_connect( + struct bs_signal *signal_ptr, + struct bs_test_listener *test_listener_ptr) +{ + bs_signal_connect( + signal_ptr, + &test_listener_ptr->listener, + _bs_test_listener_notify); +} + +/* ------------------------------------------------------------------------- */ +void bs_test_listener_disconnect( + struct bs_signal *signal_ptr, + struct bs_test_listener *test_listener_ptr) +{ + bs_signal_disconnect( + signal_ptr, + &test_listener_ptr->listener); +} + +/* == Local (static) methods =============================================== */ + +/* ------------------------------------------------------------------------- */ +/** Iterator for @ref bs_dllist_for_each: Calls @ref bs_listener::notify. */ +void _bs_signal_notify_dlnode( + bs_dllist_node_t *dlnode_ptr, + void *ud_ptr) +{ + struct bs_listener *listener_ptr = BS_CONTAINER_OF( + dlnode_ptr, struct bs_listener, dlnode); + listener_ptr->notify(listener_ptr, ud_ptr); +} + +/* ------------------------------------------------------------------------- */ +void _bs_test_listener_notify( + struct bs_listener *listener_ptr, + void *data_ptr) +{ + struct bs_test_listener *tl = BS_CONTAINER_OF( + listener_ptr, struct bs_test_listener, listener); + tl->calls++; + tl->last_data_ptr = data_ptr; +} + +/* == Unit tests =========================================================== */ + +void _bs_signal_test_simple(bs_test_t *test_ptr); + +/** Unit test cases. */ +static const bs_test_case_t _bs_signal_test_cases[] = { + { true, "simple", _bs_signal_test_simple }, + BS_TEST_CASE_SENTINEL() +}; + +/** Unit tests set. */ +const bs_test_set_t bs_signal_test_set = BS_TEST_SET( + true, "signal", _bs_signal_test_cases); + +/* ------------------------------------------------------------------------- */ +/** Simple test case for the signal. */ +void _bs_signal_test_simple(bs_test_t *test_ptr) +{ + struct bs_test_listener tl1 = {}, tl2 = {}; + struct bs_signal s = {}; + + bs_test_listener_connect(&s, &tl1); + + BS_TEST_VERIFY_EQ(test_ptr, 0, tl1.calls); + BS_TEST_VERIFY_EQ(test_ptr, 0, tl2.calls); + + bs_signal_emit(&s, _bs_signal_test_simple); + BS_TEST_VERIFY_EQ(test_ptr, 1, tl1.calls); + BS_TEST_VERIFY_EQ(test_ptr, _bs_signal_test_simple, tl1.last_data_ptr); + BS_TEST_VERIFY_EQ(test_ptr, 0, tl2.calls); + + bs_test_listener_connect(&s, &tl2); + bs_signal_emit(&s, NULL); + BS_TEST_VERIFY_EQ(test_ptr, 2, tl1.calls); + BS_TEST_VERIFY_EQ(test_ptr, NULL, tl1.last_data_ptr); + BS_TEST_VERIFY_EQ(test_ptr, 1, tl2.calls); + BS_TEST_VERIFY_EQ(test_ptr, NULL, tl2.last_data_ptr); + + bs_test_listener_disconnect(&s, &tl1); + bs_signal_emit(&s, NULL); + BS_TEST_VERIFY_EQ(test_ptr, 2, tl2.calls); + + bs_test_listener_clear(&tl1); + BS_TEST_VERIFY_EQ(test_ptr, 0, tl1.calls); + bs_test_listener_clear(&tl2); + BS_TEST_VERIFY_EQ(test_ptr, 0, tl2.calls); +} + +/* == End of signal.c ====================================================== */ diff --git a/tests/libbase_test.c b/tests/libbase_test.c index c25cd69..bac7488 100644 --- a/tests/libbase_test.c +++ b/tests/libbase_test.c @@ -20,6 +20,7 @@ */ #include +#include #include #include #include @@ -269,6 +270,7 @@ int main(int argc, const char **argv) &bs_ptr_stack_test_set, &bs_ptr_vector_test_set, &bs_ref_test_set, + &bs_signal_test_set, &bs_strutil_test_set, &bs_subprocess_extra_test_set, &bs_subprocess_test_set, From 9306cfe28eae12df07659c618643f374fe0ff4c1 Mon Sep 17 00:00:00 2001 From: Philipp Kaeser Date: Mon, 24 Aug 2026 20:43:46 +0200 Subject: [PATCH 2/2] def: Makes BS_CONTAINER_OF NULL-safe. --- include/libbase/def.h | 17 +++++++++++++---- src/signal.c | 10 ++++++++++ 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/include/libbase/def.h b/include/libbase/def.h index 6943fbc..4b8ce04 100644 --- a/include/libbase/def.h +++ b/include/libbase/def.h @@ -58,11 +58,20 @@ }) #endif // !defined(BS_MAX) -/** Helper to retrieve the base container, given a pointer to an element. */ +/** + * Helper to retrieve the base container, given a pointer to an element. + * + * NULL-safe, ie. returns NULL if elem_ptr is NULL. + */ #define BS_CONTAINER_OF(elem_ptr, container_type, elem_field) \ - (container_type*)( \ - (uint8_t*)(elem_ptr) - \ - offsetof(container_type, elem_field)) + ({ \ + __typeof__(elem_ptr) __elem_ptr = (elem_ptr); \ + (NULL == __elem_ptr ? \ + (container_type*)__elem_ptr : \ + (container_type*)( \ + (uint8_t*)(elem_ptr) - \ + offsetof(container_type, elem_field))); \ + }) #endif /* __LIBBASE_DEF_H__ */ /* == End of def.h ========================================================= */ diff --git a/src/signal.c b/src/signal.c index 561ca1c..82f90cc 100644 --- a/src/signal.c +++ b/src/signal.c @@ -144,6 +144,16 @@ void _bs_signal_test_simple(bs_test_t *test_ptr) bs_test_listener_connect(&s, &tl1); + BS_TEST_VERIFY_EQ( + test_ptr, + &tl1, + BS_CONTAINER_OF(&tl1.calls, struct bs_test_listener, calls)); + void *p = NULL; + BS_TEST_VERIFY_EQ( + test_ptr, + NULL, + BS_CONTAINER_OF(p, struct bs_test_listener, calls)); + BS_TEST_VERIFY_EQ(test_ptr, 0, tl1.calls); BS_TEST_VERIFY_EQ(test_ptr, 0, tl2.calls);