Saturday, March 29, 2025

Designing an Event-Driven System in C++

Event-driven architecture is a fundamental paradigm in software design where components communicate through events rather than direct method calls. This article breaks down a professional implementation of an event system in C++ using two key files:

1.    event_system.h: Core event management classes.

2.    domain.cpp: Domain-specific events and usage examples.

The source codes can be found in github. Here I just want to highlight a few points that are good to me:

·         Modular Design: Components communicate without tight coupling.

·         Modern C++ Features: Templates, RAII, and lambda expressions.

·         Professional Patterns: Type erasure, publisher-subscriber.

·         Code Flow Visualization:

 [EventsManager]

   |--- stores --> [vector<unique_ptr<ISubscription>>]

                         |

                         |--- contains --> [SubscriptionWrapper<Event<int>>]

                         |                [SubscriptionWrapper<Event<string>>]

                         |                [SubscriptionWrapper<Event<CustomType>>]

                         |

                         |--- On destruction -->

                               [~ISubscription()] --> [~SubscriptionWrapper()] --> [~Event<T>::Subscription()]

 

The rest of this article is generated by deepseek:

Part 1: Core Concepts (event_system.h)

Part 2: Domain Implementation (domain.cpp)

Part 3: Type Erasure Explained: The Power of ISubscription with a Single Virtual Destructor

This design elegantly balances flexibility and type safety, demonstrating how minimal interfaces can enable powerful abstractions!

Part 1: Core Concepts (event_system.h)

1.1 The Event Template Class

Purpose: Create type-safe, reusable events that can carry data.

template <typename... Args>

class Event { /*...*/ };

  • Template ParametersArgs... allow events to carry any data type.
  • Key Features:
    • subscribe(): Registers callbacks and returns a Subscription.
    • trigger(): Broadcasts events to all subscribers.
    • Automatic unsubscription via RAII.

1.2 The Subscription Class

Purpose: Manage callback lifetimes using RAII (Resource Acquisition Is Initialization.

class Subscription : public ISubscription { /*...*/ };

  • RAII Principle: Automatically unsubscribes when destroyed.
  • Move Semantics: Ensures safe ownership transfer (no copying allowed).

1.3 The EventsManager Class

Purpose: Centralize subscription management.

class EventsManager { /*...*/ };

  • Type Erasure: Uses ISubscription and SubscriptionWrapper to store heterogeneous subscriptions.
  • Lifetime Control: All subscriptions are destroyed when the manager is destroyed.

Part 2: Domain Implementation (domain.cpp)

2.1 Defining Event Types

struct TemperatureEvent { double value; };

struct DoorStatusEvent { bool isOpen; int sensorId; };

  • Best Practice: Use simple structs for event data to ensure immutability.

2.2 Creating Event Instances

Event<TemperatureEvent> temperatureEvent;

Event<DoorStatusEvent> doorStatusEvent;

  • Global vs. Local: These are global here for simplicity, but in larger projects, consider encapsulating them within classes.

2.3 Publisher-Subscriber Pattern

Example: Temperature Sensor (Publisher)

void update(double temp) {

    temperatureEvent.trigger(TemperatureEvent{temp});

}

Example: Climate Controller (Subscriber)

em.subscribe(temperatureEvent, [this](const TemperatureEvent& e) {

    adjustSystem(e.value);

});


Key Design Principles

1. Decoupling Components

  • Publishers (e.g., TemperatureSensor) don’t know about subscribers (e.g., ClimateController).
  • All communication happens through events.

2. RAII for Resource Management

  • Subscriptions automatically clean up when they go out of scope:

~Subscription() { if(event_) event_->unsubscribe(id_); }

3. Type Safety

  • Templates ensure compile-time checks:

// Compiler enforces correct event data types:

em.subscribe(temperatureEvent, [](const DoorStatusEvent& e) { /*...*/ }); // Error!

4. Scalability

  • Add new events without modifying existing code:

Event<NetworkPacket> networkEvent;  // New event type

 

PART 3: Type Erasure Explained: The Power of ISubscription with a Single Virtual Destructor

What is Type Erasure?

Type erasure is a design pattern that allows you to work with heterogeneous types through a uniform interface, while hiding ("erasing") their specific type information. It’s like putting different objects into a black box—the outside code interacts with the box, not the objects inside.

Key Characteristics:

  • Abstraction: Treat different types as if they were the same.
  • Encapsulation: Hide type-specific details behind a common interface.
  • Runtime Polymorphism: Resolve behavior at runtime (via virtual functions).

The ISubscription Class: Minimalist Type Erasure

In the event system code, ISubscription is the cornerstone of type erasure. Let’s break down its design:

Class Definition:

class ISubscription {

public:

    virtual ~ISubscription() = default;  // Single virtual destructor

};

Why This Works:

  1. Common Interface:
    All subscription types (e.g., Event<int>::Subscription, Event<string>::Subscription) inherit from ISubscription. This allows them to be stored in a single container:

std::vector<std::unique_ptr<ISubscription>> subscriptions_;

  1. Virtual Destructor:
    • Ensures that when a unique_ptr<ISubscription> is destroyed, the correct derived class destructor is called.
    • Triggers the destructor of the actual Event<T>::Subscription, which unsubscribes the callback.

How Type Erasure is Achieved

Step 1: Type-Specific Wrapper (SubscriptionWrapper)

For each event type Event<T>, we create a templated wrapper:

template <typename EventType>

struct SubscriptionWrapper : public ISubscription {

    typename EventType::Subscription sub;  // Concrete subscription

    SubscriptionWrapper(typename EventType::Subscription&& s)

        : sub(std::move(s)) {}

};

Step 2: Erase the Type

When storing subscriptions:

void EventsManager::subscribe(EventType& event, Callback&& cb) {

    auto sub = event.subscribe(std::forward<Callback>(cb));

    subscriptions_.emplace_back(

        std::make_unique<SubscriptionWrapper<EventType>>(std::move(sub))

    );

}

  • The SubscriptionWrapper<EventType> is created for a specific event type (e.g., Event<int>).
  • It is then upcast to ISubscription* and stored in the subscriptions_ vector.

Step 3: Type Recovery at Destruction

When subscriptions_ is cleared:

  1. The unique_ptr<ISubscription> calls the virtual destructor of ISubscription.
  2. Since SubscriptionWrapper inherits from ISubscription, its destructor is invoked.
  3. This destroys the wrapped EventType::Subscription, which in turn unsubscribes the callback.

Why Only a Destructor?

The ISubscription interface needs only a virtual destructor because:

  1. Minimal Contract: The only required operation is proper destruction.
  2. No Other Operations: The EventsManager doesn’t need to interact with subscriptions—it just needs to manage their lifetimes.
  3. Zero Overhead: No virtual function calls except during destruction.

Analogy: Library Book Returns

Imagine a library (EventsManager) that lends books (subscriptions) of various genres (event types). The librarian doesn’t care what genre a book is—they just need to know:

  1. How to shelve it (store in a vector<ISubscription*>).
  2. How to dispose of it (call the virtual destructor).

The genre-specific return process (unsubscribing) is handled automatically when the book is destroyed!


Key Takeaways

  1. Type Erasure Pattern:
    • Use a base class (ISubscription) to unify heterogeneous types.
    • Leverage virtual destructors for type-specific cleanup.
  2. Advantages:
    • Decoupling: The EventsManager doesn’t know about specific event types.
    • Scalability: Add new event types without modifying the manager.
    • Resource Safety: RAII ensures no leaked subscriptions.
  3. When to Use:
    • Managing objects with heterogeneous types but common lifecycle needs.
    • Hiding implementation details from client code.

 

Saturday, March 15, 2025

Type Misalignment Mirage

Some time ago, I developed a small Windows tool to measure how long a thread took to test an external device. The project intrigued me due to a particularly tricky bug that took me over an hour to resolve. During that time, I consulted ChatGPT for assistance, but it failed to provide useful solutions.

Today, I decided to try DeepSeek instead. I copied the same question I had asked ChatGPT, and amusingly, DeepSeek delivered the correct answer after just 137 seconds of processing. From now on, I’ll be making DeepSeek my first go-to resource for troubleshooting.

In this article, I’ll share the problem I encountered, how I ultimately solved it, and—of course—the first time and correct answer provided by DeepSeek.

Question (to ChatGPT and DeepSeek):

This is Visual C++ Win32 MFC application, why following function got GUI update with "Multicast stopped: 57730ms, sent 0 frames":  

void threadFunction()

{

    // Record the start time using high-resolution clock

    auto startTime = std::chrono::high_resolution_clock::now();

    int nSentCount = 0;

    // Simulate some work by making the thread sleep for 2 seconds

    // thread running ...,  std::this_thread::sleep_for(std::chrono::seconds(2));

    // nSentCount++;  // Increment the sent count

    // Record the end time

    auto endTime = std::chrono::high_resolution_clock::now();

    // Calculate the duration in milliseconds

    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(endTime - startTime).count();

    CString info;

    if (nSentCount > 1) info.Format(_T("Multicast stopped: %ldms, sent %ld frames"), duration, nSentCount);

    else info.Format(_T("Multicast stopped"));

    pView->UpdateScreenInfo(EVENT_INFO, info);

}

 

Troubleshooting

Yes, I was shocked by the result—I couldn’t believe what I was seeing. The program sends multicast packets and increments nSentCount with each successful transmission. I reviewed the code repeatedly, scrutinizing the socket implementation, multicast configuration, and potential memory corruption issues, but found no clues.

Frustrated, I turned to ChatGPT. It suggested potential issues with the MFC GUI update mechanism but completely overlooked the core problem of the interface displaying "0 frames." When I explicitly asked about the "0 frames" display, the responses I received remained irrelevant.

1.  Initialize nSentCount Appropriately: …

2.  Review Modifications to nSentCount: …

3.  Implement Logging: …

Eventually, I narrowed my focus to the CString implementation. By swapping the two parameters, I discovered that nSentCount was accurate, but the duration displayed an unexpected value. To verify this, I tested the behavior using the following source code:

int main()

{

    long long duration = 1234567890;

    int nSentCount = 321;

    printf("test printf with ld: %ldms, sent %ld frames\n", duration, nSentCount);

    printf("test printf with lld: %lldms, sent %ld frames\n", duration, nSentCount);

    return 0;

}

Compile and run:

test printf with ld: 2123683780ms, sent 1234567890 frames   --- wrong

test printf with lld: 1234567890ms, sent 321 frames         --- correct

 

DeepSeek’s Answer

The issue arises due to a combination of incorrect UI thread access and logical errors in your code. Here's the breakdown and solution:

UI Update from Worker Thread:

Problem: MFC requires GUI updates to occur on the main UI thread. Your threadFunction (running on a worker thread) directly calls pView->UpdateScreenInfo(), leading to undefined behavior.

Fix: Use PostMessage to send a message to the main UI thread, which then handles the update.

Incorrect Format String Condition:

Problem: The message shows details even when nSentCount is 0. Your code's if (nSentCount > 1) skips the details for counts ≤1, but the output suggests the condition is not met.

Fix: Check if nSentCount > 0 instead to include single-frame sends.

Uncommented Work Simulation:

Problem: The sleep and increment are commented, so nSentCount remains 0, and duration is negligible. The actual code likely has these lines uncommented but might have synchronization issues.

Fix: Uncomment the work simulation and ensure proper thread synchronization.

Corrected Code:

// In your worker thread function

void threadFunction()

{

    auto startTime = std::chrono::high_resolution_clock::now();

    int nSentCount = 0;

    // Simulate work (uncommented)

    std::this_thread::sleep_for(std::chrono::seconds(2));

    nSentCount++;  // Increment sent count

    auto endTime = std::chrono::high_resolution_clock::now();

    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(endTime - startTime).count();

    // Post message to UI thread

    CString info;

    if (nSentCount > 0)

        info.Format(_T("Multicast stopped: %lldms, sent %d frames"), duration, nSentCount);

    else

        info = _T("Multicast stopped");

    // Assuming pView is a valid pointer and you have a custom message handler

    ::PostMessage(pView->GetSafeHwnd(), WM_UPDATE_UI, (WPARAM)EVENT_INFO, (LPARAM)new CString(info));

}

 

// In your view class header (e.g., CMyView.h)

#define WM_UPDATE_UI (WM_USER + 1)

afx_msg LRESULT OnUpdateUI(WPARAM wParam, LPARAM lParam);

// In your view class implementation (e.g., CMyView.cpp)

BEGIN_MESSAGE_MAP(CMyView, CView)

    ON_MESSAGE(WM_UPDATE_UI, OnUpdateUI)

END_MESSAGE_MAP()

LRESULT CMyView::OnUpdateUI(WPARAM wParam, LPARAM lParam)

{

    EventType type = static_cast<EventType>(wParam);

    CString* pInfo = reinterpret_cast<CString*>(lParam);

    UpdateScreenInfo(type, *pInfo);

    delete pInfo; // Clean up the allocated string

    return 0;

}

 

Key Changes:

UI Thread Safety: Uses PostMessage to marshal data to the UI thread.

Condition Check: Updates the condition to nSentCount > 0 to handle single frames.

Correct Format Specifiers: Uses %lld for duration (64-bit integer) and %d for nSentCount.

Memory Management: Allocates CString dynamically to prevent stack corruption.

Additional Checks:

Ensure pView is valid and not destroyed before the message is processed.

Handle thread synchronization if threadFunction runs multiple times or in a loop.

 

What lesson we learned (by DeepSeek)

Your experience is a microcosm of software engineering: even "trivial" code can harbor insidious bugs, and rigor (in testing, tooling, and humility) is the antidote. Celebrate the solve – it’s these battles that keep the craft alive and rewarding. 🛠

你的这段经历是软件工程的缩影:即便是看似简单的代码,也可能暗藏隐蔽的缺陷,而唯有通过严谨(测试、工具链和谦逊的态度)才能将其化解。庆祝这次问题的解决吧——正是这些战斗,让这门技艺永葆活力并充满回报。

Sunday, March 2, 2025

An overengineering example to set a packet frame

In this article, I want to share an overengineering design that not necessary for a simple use case. However, it would be a good design for a complicated system if such packet is used for many interfaces.

Target: to set a packet frame with some header, some payload, in the following format:

Packet Header:

Source      : DeviceA

Destination : DeviceB

Sequence    : 42

Checksum    : 13

Message Type: 1

Full frame (28 bytes):

01 2a 44 65 76 69 63 65 41 00 00 00 44 65 76 69 63 65 42 00 00 00 aa 12 34 56 ff 0d

Let's see how simple a C-style will be:

int setPacketFrame(unsigned char* acData, unsigned char ucType)

{

    acData[0] = ucType;

    acData[1] = getSequenceNum();  //

    snprintf(acData+2, 10, "%s", SOURCE_DEV);

    snprintf(acData+12, 10, "%s", DEST_DEV);

    acData[22] = getReq();  //payload starts here

    acData[23] = getResult();

    acData[24] = getReason();

    acData[25] = getSomething1();

    acData[26] = getSomething2();

    acData[27] = calcBcc(acData+1, 12);

    return 28;

}

What do you think of this function? Look forward to your opinions on it.

Let's check the C++ complicated design, to hit the nut directly, see the flowchart as follows.

 


And see all the classes:

 

Simply speaking, this design is structured to build and finalize a network packet composed of a fixed header, a variable payload, and a checksum appended at the end. It uses a combination of low-level byte manipulation (via the PacketBuffer class) and higher-level abstractions (via the NetworkPacket interface and its implementation in ContiguousPacket) to offer a fluent API for constructing packets. 

Some highlights:

  • Abstraction Layers:
    Low-level memory manipulation is abstracted away using PacketBuffer, while higher-level packet-building logic is encapsulated in NetworkPacket and ContiguousPacket.
  • Fluent Interface:
    Chained method calls allow for a compact and readable way to set header fields and payloads.
  • Extensibility:
    The use of an abstract base class and cloning via CRTP facilitates the extension of the packet system to support different types of packets if needed.
  • You may find the source code from https://github.com/happytong/PacketWrapper/blob/main/packet_wrapper.cpp.


    How to Design a General I2C Detection Tool in QNX

    When developing embedded systems on QNX 6.6, I2C communication is often a critical component for interfacing with sensors, GPIO expanders, a...