├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── clib.json ├── index.hxx ├── perf └── index.cxx └── test └── index.cxx /.gitignore: -------------------------------------------------------------------------------- 1 | perf_runner 2 | test_runner 3 | test/index.dSYM/ 4 | perf/index.dSYM/ 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: cpp 2 | 3 | script: build run test 4 | 5 | compiler: clang 6 | 7 | os: 8 | - linux 9 | - osx 10 | 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Paolo Fragomeni 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CXX = clang++ 2 | STD_VERSION = -std=c++2a 3 | 4 | COMMON_FLAGS = -Wall -Wextra -I.. 5 | 6 | TEST_DEBUG_FLAGS = -g 7 | TEST_SANITIZE_FLAGS = -fsanitize=address -fno-omit-frame-pointer 8 | TEST_OPTIMIZE_FLAGS = -O1 9 | TEST_CXXFLAGS = $(STD_VERSION) $(COMMON_FLAGS) $(TEST_DEBUG_FLAGS) $(TEST_SANITIZE_FLAGS) $(TEST_OPTIMIZE_FLAGS) 10 | TEST_LDFLAGS = $(TEST_SANITIZE_FLAGS) # Sanitizer flags often needed at link time too 11 | TEST_LIBS = -pthread # For std::thread, std::atomic 12 | 13 | # Performance-specific flags 14 | PERF_OPTIMIZE_FLAGS = -O3 -DNDEBUG # -DNDEBUG to disable asserts and other debug code 15 | PERF_CXXFLAGS = $(STD_VERSION) $(COMMON_FLAGS) $(PERF_OPTIMIZE_FLAGS) 16 | PERF_LDFLAGS = 17 | PERF_LIBS = -pthread 18 | 19 | TEST_RUNNER = test_runner 20 | PERF_RUNNER = perf_runner 21 | 22 | TEST_SOURCES = test/index.cxx 23 | PERF_SOURCES = perf/index.cxx # Assuming your perf tests are here 24 | 25 | .PHONY: all test perf clean 26 | 27 | all: test perf 28 | 29 | ### Build and run tests 30 | test: $(TEST_RUNNER) 31 | @echo "Running tests..." 32 | ./$(TEST_RUNNER) 33 | 34 | ### Build the test runner 35 | $(TEST_RUNNER): $(TEST_SOURCES) index.hxx 36 | @echo "Building test runner..." 37 | $(CXX) $(TEST_CXXFLAGS) $(TEST_SOURCES) -o $(TEST_RUNNER) $(TEST_LDFLAGS) $(TEST_LIBS) 38 | 39 | ### Build and run performance benchmarks 40 | perf: $(PERF_RUNNER) 41 | @echo "Running performance benchmarks..." 42 | ./$(PERF_RUNNER) 43 | 44 | ### Build the performance runner 45 | $(PERF_RUNNER): $(PERF_SOURCES) index.hxx 46 | @echo "Building performance runner..." 47 | $(CXX) $(PERF_CXXFLAGS) $(PERF_SOURCES) -o $(PERF_RUNNER) $(PERF_LDFLAGS) $(PERF_LIBS) 48 | 49 | ### Clean up build artifacts 50 | clean: 51 | @echo "Cleaning up..." 52 | rm -f $(TEST_RUNNER) $(PERF_RUNNER) 53 | # Add any other object files or build artifacts if necessary: rm -f *.o 54 | 55 | ### Usage: 56 | # make -> builds and runs tests, then builds and runs perf benchmarks 57 | # make test -> builds and runs only tests 58 | # make perf -> builds and runs only perf benchmarks 59 | # make clean -> removes executables 60 | 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SYNOPSIS 2 | A minimal, thread-safe event emitter for `C++`. 3 | 4 | # DESCRIPTION 5 | This emitter allows you to emit and receive arbitrary/variadic paramaters of 6 | equal type. Captures are also allowed. 7 | 8 | ```c++ 9 | #include "index.hxx" 10 | 11 | int main() { 12 | 13 | EventEmitter ee; 14 | 15 | ee.on("hello", [&](string name, int num) { 16 | // ...do something with the values. 17 | }); 18 | 19 | ee.emit("hello", "beautiful", 100); 20 | } 21 | ``` 22 | 23 | # MOTIVATION 24 | - **Decoupling Code:** Components can communicate without direct dependencies. An event emitter acts as a central hub, allowing different parts of your application to react to events without needing to know about the originator of the event or other listeners. This promotes cleaner, more modular, and maintainable code. 25 | - **Simplified Event Handling:** Provides a straightforward API for registering listeners and emitting events, abstracting away the complexities of manual callback management. 26 | - **Asynchronous-like Patterns:** Facilitates an event-driven architecture, which is excellent for handling responses to actions, state changes, or any scenario where multiple parts of an application need to react to a specific occurrence. 27 | - **Flexibility:** Supports various C++ callable types for listeners, including lambdas, free functions, functors, and `std::function` objects. 28 | - **Familiarity:** Implements a pattern common in many other programming environments (like Node.js and browser JavaScript), making it intuitive for event-driven async programs. 29 | - **Modern C++:** Built using modern C++ features (`std::function`, `std::any`, templates, type traits) for a balance of type safety (within the bounds of type erasure) and flexibility. 30 | - **Header-Only:** Easy to integrate into any C++ project by simply including the header file. 31 | 32 | # FEATURES 33 | - Register multiple listeners for the same event name. 34 | - Register listeners that are automatically removed after being called once (`once`). 35 | - Emit events with an arbitrary number of arguments of various types. 36 | - Remove all listeners for a specific event name (`off(eventName)`). 37 | - Remove all listeners from the emitter for all events (`off()`). 38 | - Query the current number of active listeners (`listeners()`). 39 | - Configurable `maxListeners` threshold with a warning for potential memory leaks if too many listeners are added. 40 | - Header-only library for easy integration. 41 | 42 | # INSTALL 43 | 44 | ``` 45 | clib install heapwolf/cxx-eventemitter 46 | ``` 47 | 48 | # TEST 49 | 50 | ``` 51 | cmake test 52 | cmake perf 53 | ``` 54 | 55 | # USAGE 56 | 57 | ## Creating an EventEmitter 58 | 59 | First, include the header and create an instance of EventEmitter: 60 | 61 | ```c++ 62 | #include "index.hxx" // Or your path to events.h 63 | // ... 64 | 65 | EventEmitter ee; 66 | ``` 67 | 68 | ## Listening for Events: `on(eventName, callback)` 69 | Registers a callback function to be executed when an event with the specified eventName is emitted. You can register multiple listeners for the same event. 70 | 71 | - `eventName (std::string)`: The name of the event to listen for. 72 | 73 | - `callback (Callable)`: A function-like object (lambda, functor, function pointer, std::function) that will be invoked when the event is emitted. The arguments of the callback should match the arguments passed during emit. Callback return values are ignored. 74 | 75 | ```c++ 76 | ee.on("user_login", [](int userId, const std::string& username) { // Listener 1 77 | std::cout << "User logged in: ID=" << userId << ", Name=" << username << std::endl; 78 | }); 79 | 80 | ee.on("user_login", [](int userId, const std::string& /*username*/) { // Listener 2 for the same event 81 | std::cout << "Logging login activity for user ID: " << userId << std::endl; 82 | }); 83 | ``` 84 | 85 | ## Listening for an Event Once: `once(eventName, callback)` 86 | Registers a callback function that will be executed at most once for the specified eventName. After the callback is invoked for the first time, it is automatically unregistered. 87 | 88 | - `eventName (std::string)`: The name of the event. 89 | 90 | - `callback (Callable)`: The listener to execute once. 91 | 92 | ```c++ 93 | ee.once("app_initialized", []() { 94 | std::cout << "Application initialized (this message appears only once)." << std::endl; 95 | }); 96 | ``` 97 | 98 | ## Emitting Events: `emit(eventName, args...)` 99 | 100 | Emits an event with the given eventName, optionally passing arguments (args...) to all registered listeners for that event. 101 | 102 | - `eventName (std::string)`: The name of the event to emit. 103 | 104 | - `args... (Variadic)`: The arguments to pass to the listener callbacks. The types of these arguments should correspond to what the listeners expect. 105 | 106 | ```c++ 107 | // Emit the "user_login" event (both registered listeners will be called) 108 | int currentUserId = 101; 109 | std::string currentUsername = "Alice"; 110 | ee.emit("user_login", currentUserId, currentUsername); 111 | 112 | // Emit the "app_initialized" event (the 'once' listener will be called and then removed) 113 | ee.emit("app_initialized"); 114 | ee.emit("app_initialized"); // The 'once' listener will not be called again 115 | ``` 116 | 117 | ## Removing Listeners: `off(eventName)` 118 | 119 | Removes all listeners registered for the specified eventName. 120 | 121 | - `eventName (std::string)`: The name of the event whose listeners should be removed. 122 | 123 | ```c++ 124 | ee.off("user_login"); // All listeners for "user_login" are removed. 125 | ee.emit("user_login", 202, "Bob"); // No listeners will be called. 126 | 127 | off() 128 | ``` 129 | 130 | Removes all listeners for all events from the EventEmitter instance. 131 | 132 | ```c++ 133 | ee.on("eventA", [](){}); 134 | ee.on("eventB", [](){}); 135 | std::cout << "Listeners before global off(): " << ee.listeners() << std::endl; 136 | ee.off(); 137 | std::cout << "Listeners after global off(): " << ee.listeners() << std::endl; // Should be 0 138 | ee.emit("eventA"); // No listeners called 139 | ee.emit("eventB"); // No listeners called 140 | ``` 141 | 142 | ## Getting Listener Count: `listeners()` 143 | 144 | Returns the total number of active listeners currently registered with the EventEmitter across all event names. 145 | 146 | int count = ee.listeners(); 147 | std::cout << "Total active listeners: " << count << std::endl; 148 | 149 | ## Preventing leaks 150 | 151 | A public integer property that defaults to 10. If you add more than maxListeners listeners (total across all events), a warning will be printed to std::cout suggesting a potential memory leak. This is a safeguard, not a hard limit; listeners can still be added beyond this count. 152 | 153 | ```c++ 154 | EventEmitter myEmitter; 155 | std::cout << "Default maxListeners: " << myEmitter.maxListeners << std::endl; // Outputs 10 156 | myEmitter.maxListeners = 5; // You can change it 157 | 158 | for (int i = 0; i < 6; ++i) { 159 | // Assuming a unique event name for each to demonstrate the total count leading to warning 160 | myEmitter.on("some_event_" + std::to_string(i), [](){}); 161 | // A warning will be printed when the 6th listener is added. 162 | } 163 | ``` 164 | 165 | ## Callback Signatures and Argument Handling 166 | 167 | Argument Matching: When you emit an event with certain arguments (e.g., emit("event", 10, "hello")), your listeners registered for "event" should expect compatible arguments (e.g., [](int i, std::string s){...}). The library uses std::decay_t on argument types for internal storage and retrieval matching, which generally means value types, pointers, or references will be handled robustly. For instance, if a listener expects const std::string&, passing a std::string literal or std::string object to emit will work. 168 | 169 | Return Values: Any value returned by a listener callback is ignored by the EventEmitter. Callbacks are typically used for their side effects. 170 | 171 | Supported Callables: You can use lambdas (recommended for conciseness and capturing context), free functions, function objects (functors), and std::function objects as callbacks. 172 | -------------------------------------------------------------------------------- /clib.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cxx-eventemitter", 3 | "version": "0.1.0", 4 | "description": "EventEmitter for C++", 5 | "repo": "heapwolf/cxx-eventemitter", 6 | "license": "MIT", 7 | "keywords": [ 8 | "event", 9 | "listener", 10 | "eventemitter", 11 | "emitter", 12 | "observe", 13 | "subscribe" 14 | ], 15 | "src": [ 16 | "index.hxx" 17 | ], 18 | "dependencies": {} 19 | } 20 | -------------------------------------------------------------------------------- /index.hxx: -------------------------------------------------------------------------------- 1 | #ifndef __EVENTS_H_ 2 | #define __EVENTS_H_ 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | class EventEmitter { 17 | struct ListenerWrapper { 18 | std::any callback; 19 | bool is_once; 20 | }; 21 | 22 | std::map> events; 23 | int _listeners = 0; 24 | mutable std::mutex mtx_; 25 | 26 | template 27 | struct traits : public traits::operator())> {}; 28 | 29 | template 30 | struct traits { 31 | using ReturnType = R; using ArgumentTypesAsTuple = std::tuple; 32 | using OriginalFunctionType = std::function; 33 | using StoredFunctionType = std::function...)>; 34 | }; 35 | template 36 | struct traits { 37 | using ReturnType = R; using ArgumentTypesAsTuple = std::tuple; 38 | using OriginalFunctionType = std::function; 39 | using StoredFunctionType = std::function...)>; 40 | }; 41 | template 42 | struct traits { 43 | using ReturnType = R; using ArgumentTypesAsTuple = std::tuple; 44 | using OriginalFunctionType = std::function; 45 | using StoredFunctionType = std::function...)>; 46 | }; 47 | template 48 | struct traits { 49 | using ReturnType = R; using ArgumentTypesAsTuple = std::tuple; 50 | using OriginalFunctionType = std::function; 51 | using StoredFunctionType = std::function...)>; 52 | }; 53 | template 54 | struct traits { 55 | using ReturnType = R; using ArgumentTypesAsTuple = std::tuple; 56 | using OriginalFunctionType = std::function; 57 | using StoredFunctionType = std::function...)>; 58 | }; 59 | template 60 | struct traits { 61 | using ReturnType = R; using ArgumentTypesAsTuple = std::tuple; 62 | using OriginalFunctionType = std::function; 63 | using StoredFunctionType = std::function...)>; 64 | }; 65 | template 66 | struct traits> { 67 | using ReturnType = R; using ArgumentTypesAsTuple = std::tuple; 68 | using OriginalFunctionType = std::function; 69 | using StoredFunctionType = std::function...)>; 70 | }; 71 | 72 | template 73 | static typename traits>::OriginalFunctionType 74 | to_original_function(Callback&& cb) { 75 | return typename traits>::OriginalFunctionType(std::forward(cb)); 76 | } 77 | 78 | template 79 | void add_listener(const std::string& name, Callback&& cb, bool is_once_flag) { 80 | std::lock_guard lock(mtx_); 81 | 82 | if (++this->_listeners > this->maxListeners) { 83 | std::cout 84 | << "warning: possible EventEmitter memory leak detected. " 85 | << this->_listeners 86 | << " listeners added (max is " << this->maxListeners 87 | << "). For event: " << name 88 | << std::endl; 89 | } 90 | 91 | auto original_func = to_original_function(std::forward(cb)); 92 | typename traits>::StoredFunctionType storable_func = original_func; 93 | 94 | events[name].push_back({storable_func, is_once_flag}); 95 | } 96 | 97 | public: 98 | int maxListeners = 10; 99 | 100 | EventEmitter() = default; 101 | ~EventEmitter() = default; 102 | 103 | EventEmitter(const EventEmitter&) = delete; 104 | EventEmitter& operator=(const EventEmitter&) = delete; 105 | EventEmitter(EventEmitter&&) = delete; 106 | EventEmitter& operator=(EventEmitter&&) = delete; 107 | 108 | 109 | int listeners() const { 110 | std::lock_guard lock(mtx_); 111 | return this->_listeners; 112 | } 113 | 114 | template 115 | void on(const std::string& name, Callback&& cb) { 116 | add_listener(name, std::forward(cb), false /*is_once_flag*/); 117 | } 118 | 119 | template 120 | void once(const std::string& name, Callback&& cb) { 121 | add_listener(name, std::forward(cb), true /*is_once_flag*/); 122 | } 123 | 124 | void off() { 125 | std::lock_guard lock(mtx_); 126 | events.clear(); 127 | this->_listeners = 0; 128 | } 129 | 130 | void off(const std::string& name) { 131 | std::lock_guard lock(mtx_); 132 | auto it = events.find(name); 133 | if (it != events.end()) { 134 | this->_listeners -= it->second.size(); 135 | events.erase(it); 136 | } 137 | } 138 | 139 | template 140 | void emit(const std::string& name, EmitArgs&&... args) { 141 | std::vector current_call_list; 142 | bool has_any_once_listener_in_list = false; 143 | 144 | { 145 | std::lock_guard lock(mtx_); 146 | auto map_it = events.find(name); 147 | if (map_it == events.end() || map_it->second.empty()) { 148 | return; 149 | } 150 | current_call_list = map_it->second; 151 | 152 | for(const auto& wrapper : current_call_list) { 153 | if (wrapper.is_once) { 154 | has_any_once_listener_in_list = true; 155 | break; 156 | } 157 | } 158 | } 159 | 160 | std::tuple captured_args_tuple(std::forward(args)...); 161 | 162 | for (const auto& listener_entry : current_call_list) { 163 | try { 164 | using TargetFunctionType = std::function...)>; 165 | const auto& storable_func_any = listener_entry.callback; 166 | const auto& storable_func = std::any_cast(storable_func_any); 167 | 168 | std::apply([&](auto&&... tuple_args) { 169 | storable_func(std::forward(tuple_args)...); 170 | }, captured_args_tuple); 171 | 172 | } catch (const std::bad_any_cast& e) { 173 | std::cerr << "Emit error for event '" << name << "': " 174 | << "Callback signature mismatch. Details: " << e.what() 175 | << std::endl; 176 | } catch (const std::bad_function_call& e) { 177 | std::cerr << "Emit error for event '" << name << "': " 178 | << "Bad function call (e.g. empty std::function). Details: " << e.what() 179 | << std::endl; 180 | } 181 | } 182 | 183 | if (has_any_once_listener_in_list) { 184 | std::lock_guard lock(mtx_); 185 | auto map_it = events.find(name); 186 | if (map_it != events.end()) { 187 | std::vector& original_listeners_ref = map_it->second; 188 | std::size_t original_size = original_listeners_ref.size(); 189 | 190 | original_listeners_ref.erase( 191 | std::remove_if(original_listeners_ref.begin(), original_listeners_ref.end(), 192 | [](const ListenerWrapper& entry) { 193 | return entry.is_once; 194 | }), 195 | original_listeners_ref.end() 196 | ); 197 | 198 | std::size_t removed_count = original_size - original_listeners_ref.size(); 199 | this->_listeners -= removed_count; 200 | 201 | if (original_listeners_ref.empty()) { 202 | events.erase(map_it); 203 | } 204 | } 205 | } 206 | } 207 | }; 208 | 209 | #endif // __EVENTS_H_ 210 | 211 | -------------------------------------------------------------------------------- /perf/index.cxx: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "../index.hxx" 10 | 11 | std::atomic perf_callback_counter(0); 12 | 13 | const int NUM_PERF_THREADS = 4; 14 | const int EMITS_PER_THREAD_PERF = 10000; 15 | const int LISTENERS_PER_THREAD_PERF = 5; 16 | 17 | void perf_worker(EventEmitter& emitter, int thread_id) { 18 | // Each thread will work with a set of unique event names 19 | // to simulate more diverse activity, though they could also share event names. 20 | for (int i = 0; i < LISTENERS_PER_THREAD_PERF; ++i) { 21 | std::string event_name = "perf_event_t" + std::to_string(thread_id) + "_l" + std::to_string(i); 22 | emitter.on(event_name, []() { 23 | perf_callback_counter++; // Atomically increment the counter 24 | }); 25 | } 26 | 27 | // Now, each thread emits events that its listeners will catch 28 | for (int i = 0; i < EMITS_PER_THREAD_PERF; ++i) { 29 | for (int j = 0; j < LISTENERS_PER_THREAD_PERF; ++j) { 30 | std::string event_name = "perf_event_t" + std::to_string(thread_id) + "_l" + std::to_string(j); 31 | emitter.emit(event_name); 32 | } 33 | } 34 | } 35 | 36 | int main() { 37 | std::cout << "Starting Performance Test..." << std::endl; 38 | 39 | EventEmitter perf_emitter; 40 | // Set maxListeners high enough if you add many persistent listeners 41 | // Each thread adds LISTENERS_PER_THREAD_PERF 'on' listeners. 42 | perf_emitter.maxListeners = NUM_PERF_THREADS * LISTENERS_PER_THREAD_PERF + 100; 43 | 44 | perf_callback_counter = 0; 45 | 46 | std::vector threads; 47 | 48 | auto start_time = std::chrono::high_resolution_clock::now(); 49 | 50 | // Launch threads 51 | for (int i = 0; i < NUM_PERF_THREADS; ++i) { 52 | threads.emplace_back(perf_worker, std::ref(perf_emitter), i); 53 | } 54 | 55 | // Join threads 56 | for (std::thread& t : threads) { 57 | if (t.joinable()) { 58 | t.join(); 59 | } 60 | } 61 | 62 | auto end_time = std::chrono::high_resolution_clock::now(); 63 | std::chrono::duration duration_ms = end_time - start_time; 64 | 65 | long long expected_callbacks = (long long)NUM_PERF_THREADS * LISTENERS_PER_THREAD_PERF * EMITS_PER_THREAD_PERF; 66 | long long actual_callbacks = perf_callback_counter.load(); 67 | double duration_seconds = duration_ms.count() / 1000.0; 68 | 69 | std::cout << "Performance Test Finished." << std::endl; 70 | std::cout << "----------------------------------------" << std::endl; 71 | std::cout << std::fixed << std::setprecision(2); // Set precision for floating point output 72 | std::cout << "Threads: " << NUM_PERF_THREADS << std::endl; 73 | std::cout << "Listeners per Thread: " << LISTENERS_PER_THREAD_PERF << std::endl; 74 | std::cout << "Emits per Listener/Thread: " << EMITS_PER_THREAD_PERF << std::endl; 75 | std::cout << "----------------------------------------" << std::endl; 76 | std::cout << "Total Callbacks Expected: " << expected_callbacks << std::endl; 77 | std::cout << "Total Callbacks Executed: " << actual_callbacks << std::endl; 78 | std::cout << "Total Listeners in Emitter: " << perf_emitter.listeners() << std::endl; 79 | std::cout << "Duration: " << duration_ms.count() << " ms" << std::endl; 80 | 81 | // Calculate and display throughput 82 | if (duration_seconds > 0) { 83 | double callbacks_per_second = actual_callbacks / duration_seconds; 84 | // Total emits = NUM_PERF_THREADS * EMITS_PER_THREAD_PERF * LISTENERS_PER_THREAD_PERF (same as expected_callbacks in this setup) 85 | double emits_per_second = ( (double)NUM_PERF_THREADS * EMITS_PER_THREAD_PERF * LISTENERS_PER_THREAD_PERF) / duration_seconds; 86 | 87 | std::cout << "Callbacks per second: " << callbacks_per_second << std::endl; 88 | std::cout << "Emits per second: " << emits_per_second << std::endl; 89 | } else { 90 | std::cout << "Duration too short to calculate meaningful throughput." << std::endl; 91 | } 92 | std::cout << "----------------------------------------" << std::endl; 93 | 94 | if (actual_callbacks == expected_callbacks) { 95 | std::cout << "Callback count: CORRECT" << std::endl; 96 | } else { 97 | std::cout << "Callback count: INCORRECT" << std::endl; 98 | std::cerr << "Error: Expected " << expected_callbacks << " callbacks, but got " << actual_callbacks << std::endl; 99 | return 1; 100 | } 101 | 102 | return 0; 103 | } 104 | 105 | -------------------------------------------------------------------------------- /test/index.cxx: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "../index.hxx" 11 | 12 | int assertions_run = 0; 13 | int assertions_passed = 0; 14 | 15 | #define ASSERT(message, condition) do { \ 16 | assertions_run++; \ 17 | if(!(condition)) { \ 18 | std::cerr << "FAIL: " << (message) \ 19 | << " (Assertion failed: `" #condition "` was false at " << __FILE__ << ":" << __LINE__ << ")" << std::endl; \ 20 | } else { \ 21 | std::cout << "OK: " << (message) << std::endl; \ 22 | assertions_passed++; \ 23 | } \ 24 | } while(0) 25 | 26 | struct CallbackTracker { 27 | int called_count = 0; 28 | void trigger() { called_count++; } 29 | void reset() { called_count = 0; } 30 | int count() const { return called_count; } 31 | }; 32 | 33 | struct ComplexArg { 34 | int id; 35 | std::string data; 36 | ComplexArg(int i = 0, std::string d = "") : id(i), data(std::move(d)) {} 37 | // Equality operator for assertion 38 | bool operator==(const ComplexArg& other) const { 39 | return id == other.id && data == other.data; 40 | } 41 | }; 42 | 43 | struct TestFunctor { 44 | CallbackTracker& tracker; 45 | explicit TestFunctor(CallbackTracker& t) : tracker(t) {} 46 | void operator()() { tracker.trigger(); } // Overload for no arguments 47 | void operator()(int, const std::string&) { tracker.trigger(); } // Overload for specific arguments 48 | }; 49 | 50 | void example_free_function(CallbackTracker& tracker, int /*value*/) { 51 | tracker.trigger(); 52 | } 53 | 54 | int return_value_callback_side_effect = 0; 55 | int callback_returning_int(int input) { 56 | return_value_callback_side_effect = input * 10; 57 | return input * 2; 58 | } 59 | 60 | class StreamRedirector { 61 | std::ostream& stream_ref; 62 | std::streambuf* original_buffer; 63 | public: 64 | StreamRedirector(std::ostream& stream, std::streambuf* new_buffer) 65 | : stream_ref(stream), original_buffer(stream.rdbuf(new_buffer)) {} 66 | ~StreamRedirector() { 67 | stream_ref.rdbuf(original_buffer); 68 | } 69 | // Non-copyable! 70 | StreamRedirector(const StreamRedirector&) = delete; 71 | StreamRedirector& operator=(const StreamRedirector&) = delete; 72 | }; 73 | 74 | 75 | // Test #21 Worker Function, These atonics will be shared by threads in the test 76 | std::atomic async_total_on_callbacks_fired(0); 77 | std::atomic async_total_once_callbacks_fired(0); 78 | std::atomic async_total_listeners_registered(0); 79 | 80 | const int ASYNC_TEST_ITERATIONS_PER_THREAD = 50; // Number of operations per thread 81 | 82 | void async_worker_function(EventEmitter& emitter, int thread_id) { 83 | for (int i = 0; i < ASYNC_TEST_ITERATIONS_PER_THREAD; ++i) { 84 | // Create unique event names for each listener to avoid unintended shared triggers 85 | // and simplify callback counting for this specific test. 86 | std::string on_event_name = "async_on_event_t" + std::to_string(thread_id) + "_i" + std::to_string(i); 87 | std::string once_event_name = "async_once_event_t" + std::to_string(thread_id) + "_i" + std::to_string(i); 88 | 89 | // Register an 'on' listener 90 | emitter.on(on_event_name, [&]() { 91 | async_total_on_callbacks_fired++; 92 | }); 93 | async_total_listeners_registered++; 94 | 95 | // Register a 'once' listener 96 | emitter.once(once_event_name, [&]() { 97 | async_total_once_callbacks_fired++; 98 | }); 99 | async_total_listeners_registered++; 100 | 101 | // Emit events that should trigger the listeners just added 102 | emitter.emit(on_event_name); // Triggers the 'on' listener 103 | emitter.emit(once_event_name); // Triggers the 'once' listener (it will then be removed) 104 | } 105 | } 106 | 107 | 108 | int main() { 109 | /// - Test #1: Sanity and maxListeners default 110 | ASSERT("Sanity: true is true", true == true); 111 | EventEmitter ee_default; 112 | ASSERT("Default maxListeners should be 10", ee_default.maxListeners == 10); 113 | ASSERT("Default listeners should be 0", ee_default.listeners() == 0); 114 | 115 | /// - Test #2: Basic 'on' and 'emit' with lambda and arguments 116 | EventEmitter ee; 117 | CallbackTracker tracker1; 118 | int event1_arg_a = 0; 119 | std::string event1_arg_b; 120 | ee.on("event1", [&](int a, const std::string& b) { 121 | tracker1.trigger(); 122 | event1_arg_a = a; 123 | event1_arg_b = b; 124 | }); 125 | ee.emit("event1", 10, std::string("foo")); 126 | ASSERT("Basic 'on'/'emit': listener called", tracker1.count() == 1); 127 | ASSERT("Basic 'on'/'emit': first arg correct", event1_arg_a == 10); 128 | ASSERT("Basic 'on'/'emit': second arg correct", event1_arg_b == "foo"); 129 | ASSERT("Basic 'on'/'emit': listener count is 1", ee.listeners() == 1); 130 | 131 | /// - Test #3: Multiple listener registration for the same event 132 | CallbackTracker tracker1_duplicate; 133 | std::string event1_dup_arg_b; 134 | ee.on("event1", [&](int /*a*/, const std::string& b_dup) { 135 | tracker1_duplicate.trigger(); 136 | event1_dup_arg_b = b_dup; 137 | }); 138 | ASSERT("Multiple listeners: Adding second listener for same event name succeeded", true); 139 | ASSERT("Multiple listeners: Listener count is now 2 (for 'event1')", ee.listeners() == 2); 140 | 141 | tracker1.reset(); 142 | event1_arg_a = 0; event1_arg_b = ""; 143 | 144 | ee.emit("event1", 20, std::string("bar")); 145 | ASSERT("Multiple listeners: Original listener called on second emit", tracker1.count() == 1); 146 | ASSERT("Multiple listeners: Original listener arg 'a' correct on second emit", event1_arg_a == 20); 147 | ASSERT("Multiple listeners: Original listener arg 'b' correct on second emit", event1_arg_b == "bar"); 148 | ASSERT("Multiple listeners: Second listener called on emit", tracker1_duplicate.count() == 1); 149 | ASSERT("Multiple listeners: Second listener arg 'b_dup' correct", event1_dup_arg_b == "bar"); 150 | 151 | /// - Test #4: 'emit' for non-existent event 152 | ee.emit("non_existent_event", 123, "data"); 153 | ASSERT("Emit non-existent: program continues (no crash)", true); 154 | 155 | /// - Test #5: 'on' and 'emit' with no arguments 156 | CallbackTracker tracker2; 157 | ee.on("event2", [&]() { 158 | tracker2.trigger(); 159 | }); 160 | ee.emit("event2"); 161 | ASSERT("No-arg 'on'/'emit': listener called", tracker2.count() == 1); 162 | ASSERT("No-arg 'on'/'emit': listener count is 3 (2 for event1, 1 for event2)", ee.listeners() == 3); 163 | 164 | /// - Test #6: 'off(eventName)' 165 | ee.off("event1"); 166 | tracker1.reset(); 167 | tracker1_duplicate.reset(); 168 | ee.emit("event1", 30, std::string("baz")); 169 | ASSERT("off(event1): original listener not called after removal", tracker1.count() == 0); 170 | ASSERT("off(event1): second listener for event1 not called after removal", tracker1_duplicate.count() == 0); 171 | ASSERT("off(event1): listener count is 1 (event2 remains)", ee.listeners() == 1); 172 | ee.off("non_existent_to_off"); 173 | ASSERT("off(non_existent_event): listener count still 1", ee.listeners() == 1); 174 | 175 | /// - Test #7: 'once(eventName, callback)' 176 | EventEmitter ee_once; 177 | CallbackTracker tracker_once; 178 | ee_once.once("event_once", [&]() { tracker_once.trigger(); }); 179 | ASSERT("once: listener count is 1 before emit", ee_once.listeners() == 1); 180 | ee_once.emit("event_once"); 181 | ASSERT("once: listener called first time", tracker_once.count() == 1); 182 | ASSERT("once: listener count is 0 after first emit", ee_once.listeners() == 0); 183 | ee_once.emit("event_once"); 184 | ASSERT("once: listener not called second time", tracker_once.count() == 1); 185 | 186 | /// - Test #8: 'once' with arguments 187 | CallbackTracker tracker_once_args; int once_arg_val = 0; 188 | ee_once.once("event_once_args", [&](int val) { tracker_once_args.trigger(); once_arg_val = val; }); 189 | ASSERT("once_args: listener count is 1 (on ee_once)", ee_once.listeners() == 1); 190 | ee_once.emit("event_once_args", 99); 191 | ASSERT("once_args: listener called", tracker_once_args.count() == 1); 192 | ASSERT("once_args: argument correct", once_arg_val == 99); 193 | ASSERT("once_args: listener count is 0 after emit (on ee_once)", ee_once.listeners() == 0); 194 | ee_once.emit("event_once_args", 101); 195 | ASSERT("once_args: listener not called on second emit", tracker_once_args.count() == 1); 196 | 197 | /// - Test #9: 'off()' (remove all listeners) 198 | EventEmitter ee_off_all; CallbackTracker tracker_oa1, tracker_oa2, tracker_oa_once; 199 | ee_off_all.on("off_all_1", [&](){ tracker_oa1.trigger(); }); 200 | ee_off_all.on("off_all_2", [&](){ tracker_oa2.trigger(); }); 201 | ee_off_all.once("off_all_once", [&](){ tracker_oa_once.trigger(); }); 202 | ASSERT("off_all: initial listener count is 3", ee_off_all.listeners() == 3); 203 | ee_off_all.off(); 204 | ASSERT("off_all: listener count is 0 after off()", ee_off_all.listeners() == 0); 205 | ee_off_all.emit("off_all_1"); ee_off_all.emit("off_all_2"); ee_off_all.emit("off_all_once"); 206 | ASSERT("off_all: listener 1 not called after off()", tracker_oa1.count() == 0); 207 | ASSERT("off_all: listener 2 not called after off()", tracker_oa2.count() == 0); 208 | ASSERT("off_all: once listener not called after off()", tracker_oa_once.count() == 0); 209 | 210 | /// - Test #10: maxListeners warning 211 | EventEmitter ee_max; ee_max.maxListeners = 2; CallbackTracker tracker_max_cb; 212 | std::stringstream captured_cout; 213 | { 214 | StreamRedirector redirect(std::cout, captured_cout.rdbuf()); 215 | ee_max.on("max_event1", [&](){ tracker_max_cb.trigger(); }); 216 | ee_max.on("max_event2", [&](){ tracker_max_cb.trigger(); }); 217 | ee_max.on("max_event3", [&](){ tracker_max_cb.trigger(); }); 218 | } 219 | std::string cout_output = captured_cout.str(); 220 | ASSERT("maxListeners: warning message present in cout", cout_output.find("warning: possible EventEmitter memory leak detected") != std::string::npos); 221 | ASSERT("maxListeners: warning shows correct listener count (3)", cout_output.find("3 listeners added") != std::string::npos); 222 | ASSERT("maxListeners: warning shows correct maxListeners (2)", cout_output.find("max is 2") != std::string::npos); 223 | ASSERT("maxListeners: listener count is 3 after additions", ee_max.listeners() == 3); 224 | ee_max.emit("max_event1"); 225 | ee_max.emit("max_event2"); 226 | ee_max.emit("max_event3"); 227 | ASSERT("maxListeners: all 3 listeners functional despite warning", tracker_max_cb.count() == 3); 228 | 229 | /// - Test #11: Complex argument types 230 | EventEmitter ee_complex; CallbackTracker tracker_complex; ComplexArg received_arg; 231 | ComplexArg sent_arg = {123, "test_data"}; 232 | ee_complex.on("complex_event", [&](const ComplexArg& ca) { 233 | tracker_complex.trigger(); 234 | received_arg = ca; 235 | }); 236 | ee_complex.emit("complex_event", sent_arg); 237 | ASSERT("Complex Arg: listener called", tracker_complex.count() == 1); 238 | ASSERT("Complex Arg: argument received correctly", received_arg == sent_arg); 239 | 240 | /// - Test #12: Using a functor as a callback 241 | EventEmitter ee_functor; CallbackTracker tracker_functor; 242 | TestFunctor my_functor_instance(tracker_functor); 243 | ee_functor.on("functor_event_no_args", [&]() { my_functor_instance(); }); 244 | ee_functor.emit("functor_event_no_args"); 245 | ASSERT("Functor (no-args lambda): callback called", tracker_functor.count() == 1); 246 | 247 | tracker_functor.reset(); 248 | ee_functor.on("functor_event_with_args", [&](int val, const std::string& str) { my_functor_instance(val, str); }); 249 | ee_functor.emit("functor_event_with_args", 1, std::string("test")); 250 | ASSERT("Functor (with_args lambda): callback called", tracker_functor.count() == 1); 251 | 252 | /// - Test #13: Using a free function as a callback 253 | EventEmitter ee_free_func; CallbackTracker tracker_free_func; 254 | ee_free_func.on("free_func_event", [&](int val) { example_free_function(tracker_free_func, val); }); 255 | ee_free_func.emit("free_func_event", 50); 256 | ASSERT("Free function (wrapped): callback called", tracker_free_func.count() == 1); 257 | 258 | /// - Test #14: Callback returning a value 259 | EventEmitter ee_return; CallbackTracker tracker_return_val_cb; return_value_callback_side_effect = 0; 260 | ee_return.on("return_event", [&](int x) { 261 | callback_returning_int(x); 262 | tracker_return_val_cb.trigger(); 263 | }); 264 | ee_return.emit("return_event", 7); 265 | ASSERT("Return value: callback was called", tracker_return_val_cb.count() == 1); 266 | ASSERT("Return value: internal side effect of callback function correct", return_value_callback_side_effect == 70); 267 | 268 | /// - Test #15: Emit with argument type mismatch 269 | EventEmitter ee_mismatch; CallbackTracker tracker_mismatch; 270 | ee_mismatch.on("mismatch_event", [&](int /*i*/) { tracker_mismatch.trigger(); }); 271 | std::stringstream captured_cerr_mismatch; 272 | { 273 | StreamRedirector redirect(std::cerr, captured_cerr_mismatch.rdbuf()); 274 | ee_mismatch.emit("mismatch_event", "this is not an int"); 275 | } 276 | ASSERT("Type Mismatch: listener NOT called", tracker_mismatch.count() == 0); 277 | std::string cerr_output_mismatch = captured_cerr_mismatch.str(); 278 | ASSERT("Type Mismatch: error message present in cerr", cerr_output_mismatch.find("Callback signature mismatch") != std::string::npos); 279 | 280 | /// - Test #16: Modifying emitter from within a callback (removing self via 'off') 281 | EventEmitter ee_modify_self_off; CallbackTracker tracker_mod_self_off; 282 | ee_modify_self_off.on("mod_self_off", [&]() { 283 | tracker_mod_self_off.trigger(); 284 | ee_modify_self_off.off("mod_self_off"); 285 | }); 286 | ee_modify_self_off.emit("mod_self_off"); 287 | ASSERT("Modify self (off): called once", tracker_mod_self_off.count() == 1); 288 | ee_modify_self_off.emit("mod_self_off"); 289 | ASSERT("Modify self (off): not called after self-removal", tracker_mod_self_off.count() == 1); 290 | ASSERT("Modify self (off): listener count is 0", ee_modify_self_off.listeners() == 0); 291 | 292 | /// - Test #17: Modifying emitter from within a 'once' callback 293 | EventEmitter ee_modify_self_once; CallbackTracker tracker_mod_self_once; 294 | ee_modify_self_once.once("mod_self_once", [&]() { 295 | tracker_mod_self_once.trigger(); 296 | }); 297 | ee_modify_self_once.emit("mod_self_once"); 298 | ASSERT("Modify self (once): called once", tracker_mod_self_once.count() == 1); 299 | ee_modify_self_once.emit("mod_self_once"); 300 | ASSERT("Modify self (once): not called again", tracker_mod_self_once.count() == 1); 301 | ASSERT("Modify self (once): listener count is 0", ee_modify_self_once.listeners() == 0); 302 | 303 | /// - Test #18: Modifying emitter from within a callback (adding another listener) 304 | EventEmitter ee_modify_other; CallbackTracker tracker_mod_other1, tracker_mod_other2; 305 | ee_modify_other.on("mod_add", [&]() { 306 | tracker_mod_other1.trigger(); 307 | if (tracker_mod_other1.count() == 1) { 308 | ee_modify_other.on("mod_added_event", [&]() { tracker_mod_other2.trigger(); }); 309 | } 310 | }); 311 | ee_modify_other.emit("mod_add"); 312 | ASSERT("Modify other (add): first listener called", tracker_mod_other1.count() == 1); 313 | ASSERT("Modify other (add): listener count is 2 after add", ee_modify_other.listeners() == 2); 314 | ee_modify_other.emit("mod_added_event"); 315 | ASSERT("Modify other (add): newly added listener called", tracker_mod_other2.count() == 1); 316 | 317 | /// - Test #19: `once` then `on` with same event name 318 | EventEmitter ee_once_on_combo; 319 | CallbackTracker tracker_oo_once, tracker_oo_on; 320 | ee_once_on_combo.once("combo_event", [&](){ tracker_oo_once.trigger(); }); 321 | ee_once_on_combo.on("combo_event", [&](){ tracker_oo_on.trigger(); }); 322 | ASSERT("Once then On combo: Listener count is 2", ee_once_on_combo.listeners() == 2); 323 | ee_once_on_combo.emit("combo_event"); 324 | ASSERT("Once then On combo: 'once' listener called", tracker_oo_once.count() == 1); 325 | ASSERT("Once then On combo: 'on' listener called", tracker_oo_on.count() == 1); 326 | ASSERT("Once then On combo: Listener count is 1 after emit (once removed, on remains)", ee_once_on_combo.listeners() == 1); 327 | tracker_oo_once.reset(); tracker_oo_on.reset(); 328 | ee_once_on_combo.emit("combo_event"); 329 | ASSERT("Once then On combo: 'once' listener NOT called on second emit", tracker_oo_once.count() == 0); 330 | ASSERT("Once then On combo: 'on' listener called on second emit", tracker_oo_on.count() == 1); 331 | 332 | /// - Test #20: `on` then `once` with same event name 333 | EventEmitter ee_on_once_combo; 334 | CallbackTracker tracker_oo2_on, tracker_oo2_once; 335 | ee_on_once_combo.on("combo_event2", [&](){ tracker_oo2_on.trigger(); }); 336 | ee_on_once_combo.once("combo_event2", [&](){ tracker_oo2_once.trigger(); }); 337 | ASSERT("On then Once combo: Listener count is 2", ee_on_once_combo.listeners() == 2); 338 | ee_on_once_combo.emit("combo_event2"); 339 | ASSERT("On then Once combo: 'on' listener called", tracker_oo2_on.count() == 1); 340 | ASSERT("On then Once combo: 'once' listener called", tracker_oo2_once.count() == 1); 341 | ASSERT("On then Once combo: Listener count is 1 after emit", ee_on_once_combo.listeners() == 1); 342 | tracker_oo2_on.reset(); tracker_oo2_once.reset(); 343 | ee_on_once_combo.emit("combo_event2"); 344 | ASSERT("On then Once combo: 'on' listener called on second emit", tracker_oo2_on.count() == 1); 345 | ASSERT("On then Once combo: 'once' listener NOT called on second emit", tracker_oo2_once.count() == 0); 346 | 347 | /// - Test #21: Async: Concurrent on, once, emit 348 | std::cout << "\nStarting Test #23: Async Operations...\n"; 349 | EventEmitter ee_async_test; 350 | const int NUM_ASYNC_THREADS = 10; // Number of concurrent threads 351 | // Set maxListeners high enough to accommodate all 'on' listeners from this test 352 | // Each thread adds ASYNC_TEST_ITERATIONS_PER_THREAD 'on' listeners that persist. 353 | ee_async_test.maxListeners = NUM_ASYNC_THREADS * ASYNC_TEST_ITERATIONS_PER_THREAD + 10; 354 | 355 | // Reset atomic counters for this test 356 | async_total_on_callbacks_fired = 0; 357 | async_total_once_callbacks_fired = 0; 358 | async_total_listeners_registered = 0; 359 | 360 | std::vector threads; 361 | for (int i = 0; i < NUM_ASYNC_THREADS; ++i) { 362 | threads.emplace_back(async_worker_function, std::ref(ee_async_test), i); 363 | } 364 | 365 | for (std::thread& t : threads) { 366 | if (t.joinable()) { 367 | t.join(); 368 | } 369 | } 370 | 371 | int expected_on_callbacks = NUM_ASYNC_THREADS * ASYNC_TEST_ITERATIONS_PER_THREAD; 372 | int expected_once_callbacks = NUM_ASYNC_THREADS * ASYNC_TEST_ITERATIONS_PER_THREAD; 373 | // Each 'on' listener remains, each 'once' listener is removed after firing. 374 | int expected_final_listeners = NUM_ASYNC_THREADS * ASYNC_TEST_ITERATIONS_PER_THREAD; 375 | int expected_total_registrations = NUM_ASYNC_THREADS * ASYNC_TEST_ITERATIONS_PER_THREAD * 2; 376 | 377 | ASSERT("Async Test: Total 'on' callbacks fired correctly", async_total_on_callbacks_fired.load() == expected_on_callbacks); 378 | ASSERT("Async Test: Total 'once' callbacks fired correctly", async_total_once_callbacks_fired.load() == expected_once_callbacks); 379 | ASSERT("Async Test: Total listeners registered (sanity check)", async_total_listeners_registered.load() == expected_total_registrations); 380 | ASSERT("Async Test: Final listener count in emitter correct", ee_async_test.listeners() == expected_final_listeners); 381 | std::cout << "...Finished Test #23: Async Operations.\n"; 382 | 383 | std::cout << "\nSummary\n-------" << std::endl; 384 | std::cout << "Total Assertions Run: " << assertions_run << std::endl; 385 | std::cout << "Assertions Passed: " << assertions_passed << std::endl; 386 | std::cout << "Assertions Failed: " << (assertions_run - assertions_passed) << std::endl; 387 | 388 | if (assertions_passed == assertions_run) { 389 | std::cout << "\nOK!" << std::endl; 390 | return 0; 391 | } else { 392 | std::cout << "\nFAILED!" << std::endl; 393 | return 1; 394 | } 395 | } 396 | 397 | --------------------------------------------------------------------------------