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.


    Saturday, March 30, 2024

    How to draw countless lines without rendering delay in Android Phone

    If you just call the drawing API to draw lines on the screen, you may be comfortable using it when the line number is small, you will see the drawing is flowing smoothly. 

    public class DrawView extends View {

        Paint paint = new Paint();

        @Override

        public void onDraw(Canvas canvas) {

            switch (OperationCtrl.nState)

            {

                case CREATE_PATTERN:

                    DrawPattern(canvas, alTouchPoints);

                    break;

            }

        }

       private void DrawPattern(Canvas canvas, ArrayList<CustomPoint> points)

        {

                ...

            for (int i = 0; i < points.size()-1; i++) 

            {

                lines[lineIndex++] = points.get(i).x;

                lines[lineIndex++] = points.get(i).y;

                lines[lineIndex++] = points.get(i+1).x;

                lines[lineIndex++] = points.get(i+1).y;

            }

            paint.setColor(color);

            paint.setStrokeWidth(thickness);

            canvas.drawLines(lines, paint);

        }

    }

    I called this method the direct drawing method.

    However, when you create hundreds or thousands of lines, the drawing will be so laggy that your app is unable to be enjoyed.

    The solution is to use a buffer memory (bitmap) to do all the rendering jobs, then show up once to the user. 

    Put thousands of words into one sentence, a simple method can solve the rendering issue.  I called this method the bitmap methodAn additional improvement is every rendering only re-draws new lines compared with the previous bitmap. 

    public class DrawView extends View {

        private static Bitmap bufferBitmap = null;

        private static Canvas bufferCanvas = null;

        public static void createBitmap()

        {

            if (bufferBitmap != null) return;

            bufferBitmap = Bitmap.createBitmap(MainActivity.ScreenSize.x, MainActivity.ScreenSize.y, Bitmap.Config.ARGB_8888);

            bufferCanvas = new Canvas(bufferBitmap);

        }

        @Override

        public void onDraw(Canvas canvas) {

            switch (OperationCtrl.nState)

            {

                case CREATE_PATTERN:

                    DrawNewTouchPoints(canvas);

                    break;

            }

        }

    private void DrawNewTouchPoints(Canvas canvas)

        {

            //only draw new lines

           createBitmap();

            if (alTouchPoints.size() <= nLastDrawPoint+1)

            {

                canvas.drawBitmap(bufferBitmap, 0, 0, null);

                return;

            }

            while (alTouchPoints.size() > nLastDrawPoint+1) {

                nLastDrawPoint++;

                int thickness = alDrawThickness.get(nLastDrawPoint);

                int color = alDrawColor.get(nLastDrawPoint);

                int lineIndex = 0;

                float[] lines = new float[4];

                lines[lineIndex++] = alTouchPoints.get(nLastDrawPoint-1).x;

                lines[lineIndex++] = alTouchPoints.get(nLastDrawPoint-1).y;

                lines[lineIndex++] = alTouchPoints.get(nLastDrawPoint).x;

                lines[lineIndex++] = alTouchPoints.get(nLastDrawPoint).y;

                paint.setColor(color);

                paint.setStrokeWidth(thickness);

                bufferCanvas.drawLines(lines, paint);

            }

            canvas.drawBitmap(bufferBitmap, 0, 0, null);

        }

    }

    With this implementation, the playback is satisfied as expected. For example, drawing 1100 lines with the same speed, the direct drawing method took 55 seconds, while the bitmap method took 18 seconds as designed.

    My app DoodleStory using the bitmap method was published in early 2024: https://play.google.com/store/apps/details?id=com.tongs.doodlestory



    You can try it out to see the performance. The source codes can be found in GitHub.

    My other Android Apps (still available as of today):

    FunPatternWifi: Users in the same WiFi network can play this game, one creates a pattern, and then others can observe similar patterns and guess the right one. Source codes are available in GitHub.

    Pattern Creation and Guess: this is the single-user version of FunPattern. The user creates one pattern, and then the app generates a few and shows animation on the screen for the user to guess.

    Location Alarm and Measure: this app will monitor the current location, and give an alarm when entering or leaving a certain range of the preset location. It can also easily measure the distance between locations. Source codes are available in GitHub.

    Hit and Solve Maths: this app shows a block on the screen randomly, the user can try to hit and do a maths calculation for fun.




    Building on Event Handlers: Implementing Delayed Triggers in C++

    Introduction Earlier, I explained  four foundational event handler patterns  in C++—from basic callbacks to observer systems ( https://a5w...