LVGL Custom Widgets: Define Constructor, Event and Destructor Responsibilities

A reusable custom widget needs three distinct contracts: construction establishes valid private state, event handling preserves the intended base-class behavior, and destruction releases only resources owned by that class. Add a separate readiness gate when custom initialization can fail. A constructor callback returning normally does not report that every application resource was acquired.

Applicability: this article uses LVGL v9.3.0’s C class implementation and private class definitions. These internals require release-pinned maintenance. The example is a proposed composite readout with one label child and two custom-owned buffers, used by a single serialized UI owner. It contains no asynchronous producer, executable implementation or claimed test result.

Automatic construction and destruction chains flank explicit base-event delegation; a creation wrapper gates readiness and the deletion path removes children before the custom destructor.
Original lifecycle contract for a hypothetical composite readout. Proposed tests cover custom allocation failures, not universal LVGL allocation-failure recovery.

1. Specify the instance and ownership layout

For direct inheritance from lv_obj_class, put the base object at the start of the instance structure and append the readout’s fields. The tagged LED instance definition illustrates that layout. If deriving from another widget, preserve that widget’s complete instance prefix instead of assuming every base occupies only lv_obj_t.

The class descriptor contains base_class, constructor_cb, destructor_cb, event_cb and instance_size. Set the instance size to the complete custom structure. A class descriptor with insufficient storage can appear to create successfully while later private-field writes corrupt adjacent memory.

Readout field Ownership Release responsibility
Label pointer Child object owned through LVGL parent tree Normal child deletion
Copied unit-name buffer Custom class owns allocation Custom destructor
Formatting workspace Custom class owns allocation Custom destructor
Readiness state Inline scalar No separate allocation

Keep a borrowed model reference out of this minimal fixture. Adding one later requires a separate lifetime contract; the presence of a pointer in a widget does not transfer ownership automatically.

2. Keep construction valid before it becomes useful

The v9.3.0 class implementation allocates zeroed instance storage, then provides a separate initialization operation. Constructors run from base toward derived class. Destructors run in the reverse direction. The framework performs those constructor/destructor chains, so custom callbacks must not manually invoke their base callbacks a second time.

Make the readout constructor initialize scalar defaults, null resource fields and an unready state. It should leave an object that can tolerate ordinary events without dereferencing absent children or buffers. Keep fallible application setup in the public creation wrapper after normal class initialization.

The wrapper then creates the label and acquires the copied unit-name buffer and formatting workspace. Publish a usable handle only after all required stages succeed. During initialization, parent notifications or layout activity may occur; an unready event handler must remain safe before the wrapper returns.

The class callbacks have void return types, and lv_obj_class_init_obj does not provide an application-specific initialization result. Use an explicit status field and wrapper result for custom failures. Do not invent a constructor return convention that the framework never reads.

3. Delegate base events deliberately

Constructor chaining and event chaining are different mechanisms. In the event implementation, lv_obj_event_base with a class argument starts at that class’s base and finds the next event callback. A null class starts at the current target’s own class, which can reenter the custom handler if used there incorrectly.

For this readout’s ordinary event policy, call lv_obj_event_base with the class currently handling the event, check its result, and return immediately on failure. Only then perform the readout-specific action. A deliberate rendering override may require a different policy, but document it per event rather than accidentally skipping base behavior for everything.

Separate base forwarding from readiness. An unready widget can still need base layout and lifecycle processing even though its custom value formatting must be suppressed. For accepted custom events, validate the event parameter and readiness before accessing either buffer or the label.

Do not expect class-event user data to supply the custom instance fields. Resolve the current target and the correct instance type explicitly. Keep subclass behavior in the fixture too: it catches a base-dispatch call that works for the readout itself but recurses or skips a class when inherited.

4. Respect the child-before-destructor boundary

The normal object deletion path sends the deletion event, removes registered callbacks and recursively deletes children before invoking object-specific destruction. Therefore, by the time the readout destructor runs, its stored label pointer is no longer a usable child object.

Release the two custom buffers in the destructor when their fields are non-null, then clear the fields for diagnostic clarity. Do not delete or update the label there, and do not free the instance itself. The framework owns the instance and continues through the base destructor and object cleanup.

Assign each allocation one release owner. If a deletion-event callback releases a resource and the destructor also frees it, normal teardown becomes a double-free path. In this fixture, reserve final buffer release for the destructor; the deletion event may mark custom work unavailable without releasing those same allocations.

5. Test partial custom initialization explicitly

Run fault injection at the two application-owned buffer acquisitions after ordinary LVGL initialization has succeeded. First fail the unit-name copy, then allow that copy and fail the formatting workspace. In each case, keep readiness false and delete the partially prepared object through the normal object API. Expect existing children and acquired custom buffers to be released exactly once.

This bounded test does not establish that arbitrary exhaustion inside every LVGL allocation is recoverable. Some internal allocation paths rely on assertions or stronger platform assumptions. A whole-system out-of-memory policy needs an independent audit of the selected allocator and call paths.

  1. Create and delete one fully ready readout; record constructor order, owned allocations and destructor order.
  2. Exercise each custom failure point and verify no usable handle escapes the wrapper.
  3. Send ordinary events while the custom readiness flag is false; check that base processing remains valid and formatting is skipped.
  4. Delete the parent container and verify the readout follows the same release contract.
  5. Create a small subclass and confirm each lifecycle callback executes once in the expected order.

6. Preserve a class-level acceptance record

Retain the instance layout, resource table, readiness transitions and callback trace with the LVGL tag. Use host memory diagnostics for invalid accesses and release counts, then qualify the target build’s allocation behavior separately. The acceptance criterion is a reusable widget whose successful, partially initialized and inherited forms all have explicit ownership and event behavior.

Similar Posts