Skip to content

Use callbacks on heap and introduce reference counting#20

Merged
kimkulling merged 2 commits intomainfrom
bugfix/fix_callback_usage
Mar 11, 2026
Merged

Use callbacks on heap and introduce reference counting#20
kimkulling merged 2 commits intomainfrom
bugfix/fix_callback_usage

Conversation

@kimkulling
Copy link
Owner

@kimkulling kimkulling commented Mar 11, 2026

  • Add reference counting to callbacks
  • Put callback onto the heap and release them if refcount == 0;

Summary by CodeRabbit

  • Refactor

    • Improved internal callback lifetime management to enhance stability and resource cleanup across UI widgets.
  • Bug Fixes

    • Reduced risk of dangling callbacks and related UI crashes by updating ownership and destruction behavior for widget callbacks.

@coderabbitai
Copy link

coderabbitai bot commented Mar 11, 2026

Walkthrough

Introduces simple reference counting on CallbackI (mNumRefs, incRef, decRef) and updates widget code to inc/dec references. Sample switches from stack-allocated callbacks to heap-allocated callback instances to align with the new lifetime semantics.

Changes

Cohort / File(s) Summary
Callback reference-counting
src/tinyui.h
Adds uint32_t mNumRefs and incRef()/decRef() methods to CallbackI; decRef() deletes the object when count reaches zero.
Widget ownership adjustments
src/widgets.h, src/widgets.cpp
Widget destructor now calls decRef() on mCallback. Widget setters (Button, ImageButton, TreeView) call incRef() when assigning non-null callbacks.
Sample updates
samples/main.cpp
Replaces stack-allocated callback instances with heap-allocated callbacks (dynamicQuitCallback, dynamicUpdateProgressBarCallback) to match reference-counted lifetime management.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main changes: moving callbacks to the heap and adding reference counting mechanism throughout the codebase.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch bugfix/fix_callback_usage

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@sonarqubecloud
Copy link

sonarqubecloud bot commented Mar 11, 2026

Quality Gate Passed Quality Gate passed

Issues
0 New issues
5 Accepted issues

Measures
0 Security Hotspots
No data about Coverage
0.0% Duplication on New Code

See analysis details on SonarQube Cloud

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
samples/main.cpp (1)

101-102: Consider using auto for cleaner type declarations.

The type is already evident from the new expression. Using auto reduces redundancy.

♻️ Suggested change
-    CallbackI *dynamicQuitCallback = new CallbackI(quit, (void*) &ctx);
-    CallbackI *dynamicUpdateProgressBarCallback = new CallbackI(updateProgressbar, nullptr, Events::UpdateEvent);
+    auto *dynamicQuitCallback = new CallbackI(quit, (void*) &ctx);
+    auto *dynamicUpdateProgressBarCallback = new CallbackI(updateProgressbar, nullptr, Events::UpdateEvent);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@samples/main.cpp` around lines 101 - 102, Replace the explicit type
declarations for the two allocations with auto so the compiler deduces the
pointer type; e.g., change the declarations creating new CallbackI to use auto
(preferably auto* for clarity) for dynamicQuitCallback and
dynamicUpdateProgressBarCallback while keeping the constructor arguments (quit,
(void*)&ctx) and (updateProgressbar, nullptr, Events::UpdateEvent) unchanged so
the variables still point to the newly allocated CallbackI instances.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@samples/main.cpp`:
- Around line 100-105: The progressBar path leaks because Widgets::progressBar
assigns dynamicUpdateProgressBarCallback and pushes it into
ctx.mUpdateCallbackList without calling CallbackI::incRef(), leaving mNumRefs at
0 so decRef() won't delete it; update Widgets::progressBar (the assignment and
list-push logic that handles the callback) to call callback->incRef() when the
widget takes ownership (same place Widgets::button calls incRef()), or ensure
the code that adds to ctx.mUpdateCallbackList increments the refcount so
decRef() can later free the CallbackI instance.

In `@src/tinyui.h`:
- Around line 317-318: The member mNumRefs is initialized to 0 which breaks
intrusive reference-count semantics; change its initialization to 1 so that a
newly constructed callback object owns one reference by default (update the
declaration of uint32_t mNumRefs to initialize to 1); locate the member in the
callback class in src/tinyui.h and adjust the initializer for mNumRefs
accordingly so decRef() can be called without a prior explicit incRef().
- Around line 351-358: decRef() currently silently ignores calls when mNumRefs
is 0 and uses delete this, which is unsafe; change decRef to return a bool and
enforce/validate the counter: ensure mNumRefs is initialized correctly (or
assert mNumRefs>0 on entry), decrement mNumRefs and if it reaches 0 perform
deletion and return true, otherwise return false; update all callers of decRef
to handle the boolean result so they know the object may have been destroyed
(or, as an alternative, replace manual ref counting with
std::shared_ptr/std::enable_shared_from_this to eliminate delete this and manual
mNumRefs management).

In `@src/widgets.h`:
- Around line 97-100: The assigned callback stored in mCallback needs its
reference count incremented to match the existing decRef() in the destructor;
after you assign the new callback to mCallback (the same assignment around where
mCallback is set at that widget setter), immediately call mCallback->incRef() so
the object's mNumRefs is incremented and decRef() will later delete it properly;
ensure this mirrors the incRef()/decRef() pairing used in other places (e.g.,
the patterns around the earlier assignments that call incRef()).

---

Nitpick comments:
In `@samples/main.cpp`:
- Around line 101-102: Replace the explicit type declarations for the two
allocations with auto so the compiler deduces the pointer type; e.g., change the
declarations creating new CallbackI to use auto (preferably auto* for clarity)
for dynamicQuitCallback and dynamicUpdateProgressBarCallback while keeping the
constructor arguments (quit, (void*)&ctx) and (updateProgressbar, nullptr,
Events::UpdateEvent) unchanged so the variables still point to the newly
allocated CallbackI instances.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: dcea9233-a925-4f5b-b68c-0bf79d24ce4a

📥 Commits

Reviewing files that changed from the base of the PR and between c655f4f and 55daac0.

📒 Files selected for processing (3)
  • samples/main.cpp
  • src/tinyui.h
  • src/widgets.h

Comment on lines +100 to +105
// Allocate callbacks dynamically to ensure they persist during event handling
CallbackI *dynamicQuitCallback = new CallbackI(quit, (void*) &ctx);
CallbackI *dynamicUpdateProgressBarCallback = new CallbackI(updateProgressbar, nullptr, Events::UpdateEvent);

Widgets::button(7, RootPanelId, "Quit", Rect(100, 250, 100, 40), dynamicQuitCallback);
Widgets::progressBar(8, RootPanelId, Rect(100, 300, 100, 40), 50, dynamicUpdateProgressBarCallback);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check how callbacks are assigned in widget creation functions

# Search for button implementation
echo "=== Searching for Widgets::button implementation ==="
ast-grep --pattern $'ret_code Widgets::button($$$) {
  $$$
}'

# Search for progressBar implementation  
echo "=== Searching for Widgets::progressBar implementation ==="
ast-grep --pattern $'ret_code Widgets::progressBar($$$) {
  $$$
}'

Repository: kimkulling/tiny_ui

Length of output: 172


🏁 Script executed:

#!/bin/bash
# Find the relevant files
echo "=== Finding main.cpp ==="
fd -t f "main.cpp"

echo "=== Finding tinyui.h ==="
fd -t f "tinyui.h"

echo "=== Finding CallbackI class definition ==="
rg "class CallbackI|struct CallbackI" -A 20

Repository: kimkulling/tiny_ui

Length of output: 1971


🏁 Script executed:

#!/bin/bash
# Get the full CallbackI struct definition
echo "=== Full CallbackI definition from tinyui.h ==="
rg "struct CallbackI" -A 80 src/tinyui.h | head -120

echo ""
echo "=== Search for incRef and decRef methods ==="
rg "incRef|decRef" src/tinyui.h -A 3

echo ""
echo "=== Search for Widgets namespace and button/progressBar functions ==="
fd -t f "*.cpp" "*.h" -x rg "namespace Widgets|ret_code.*button|ret_code.*progressBar" {} + | head -30

Repository: kimkulling/tiny_ui

Length of output: 3404


🏁 Script executed:

#!/bin/bash
# Search for Widgets functions
echo "=== Search for button function ==="
rg "button\(" src/ -B 2 -A 15 | head -50

echo ""
echo "=== Search for progressBar function ==="
rg "progressBar\(" src/ -B 2 -A 15 | head -50

echo ""
echo "=== Search for any calls to incRef ==="
rg "incRef\(\)" src/ samples/ -B 2 -A 2

Repository: kimkulling/tiny_ui

Length of output: 4818


🏁 Script executed:

#!/bin/bash
# Get the full progressBar implementation
echo "=== Full progressBar implementation ==="
rg "ret_code Widgets::progressBar" -A 30 src/widgets.cpp

echo ""
echo "=== Check samples/main.cpp lines 95-110 ==="
sed -n '95,110p' samples/main.cpp

Repository: kimkulling/tiny_ui

Length of output: 1941


Only the progressBar callback will leak; the button callback is properly managed.

The review comment incorrectly states that incRef() is never called in Widgets::button(). The button() function does call callback->incRef() after assignment, so dynamicQuitCallback will be properly reference-counted and freed when the widget is destroyed.

However, Widgets::progressBar() does not call incRef(). It only assigns the callback and adds it to ctx.mUpdateCallbackList. This means dynamicUpdateProgressBarCallback starts with mNumRefs = 0, and when the widget is destroyed, decRef() will not delete it, causing a memory leak.

🧰 Tools
🪛 GitHub Check: SonarCloud Code Analysis

[failure] 101-101: Replace the use of "new" with an operation that automatically manages the memory.

See more on https://sonarcloud.io/project/issues?id=kimkulling_tiny_ui&issues=AZzem9Sw5gyFWmdews0o&open=AZzem9Sw5gyFWmdews0o&pullRequest=20


[warning] 102-102: Replace the redundant type with "auto".

See more on https://sonarcloud.io/project/issues?id=kimkulling_tiny_ui&issues=AZzem9Sw5gyFWmdews0n&open=AZzem9Sw5gyFWmdews0n&pullRequest=20


[failure] 102-102: Replace the use of "new" with an operation that automatically manages the memory.

See more on https://sonarcloud.io/project/issues?id=kimkulling_tiny_ui&issues=AZzem9Sw5gyFWmdews0p&open=AZzem9Sw5gyFWmdews0p&pullRequest=20


[warning] 101-101: Replace the redundant type with "auto".

See more on https://sonarcloud.io/project/issues?id=kimkulling_tiny_ui&issues=AZzem9Sv5gyFWmdews0m&open=AZzem9Sv5gyFWmdews0m&pullRequest=20

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@samples/main.cpp` around lines 100 - 105, The progressBar path leaks because
Widgets::progressBar assigns dynamicUpdateProgressBarCallback and pushes it into
ctx.mUpdateCallbackList without calling CallbackI::incRef(), leaving mNumRefs at
0 so decRef() won't delete it; update Widgets::progressBar (the assignment and
list-push logic that handles the callback) to call callback->incRef() when the
widget takes ownership (same place Widgets::button calls incRef()), or ensure
the code that adds to ctx.mUpdateCallbackList increments the refcount so
decRef() can later free the CallbackI instance.

Comment on lines +317 to +318
/// The reference count for the callback interface, used for memory management.
uint32_t mNumRefs{ 0 };
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Initialize mNumRefs to 1 for correct reference counting semantics.

With mNumRefs initialized to 0, a newly created callback requires an explicit incRef() before the first decRef() will function correctly. The standard intrusive reference counting pattern initializes the count to 1 at construction time, representing the creator's ownership.

🐛 Proposed fix
     /// The reference count for the callback interface, used for memory management.
-    uint32_t mNumRefs{ 0 };
+    uint32_t mNumRefs{ 1 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/tinyui.h` around lines 317 - 318, The member mNumRefs is initialized to 0
which breaks intrusive reference-count semantics; change its initialization to 1
so that a newly constructed callback object owns one reference by default
(update the declaration of uint32_t mNumRefs to initialize to 1); locate the
member in the callback class in src/tinyui.h and adjust the initializer for
mNumRefs accordingly so decRef() can be called without a prior explicit
incRef().

Comment on lines +351 to +358
void decRef() {
if (mNumRefs > 0) {
--mNumRefs;
if (mNumRefs == 0) {
delete this;
}
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

decRef() logic flaw and fragile delete this pattern.

Two concerns:

  1. Logic issue: If mNumRefs is 0 (initial value), decRef() silently does nothing. This means callbacks that were never incRef()'d will leak. Either fix the initial value (see above) or adjust the guard.

  2. delete this is error-prone: The caller has no indication that the object was destroyed, which can lead to use-after-free. Consider returning a boolean indicating whether the object was deleted, or using a weak reference pattern.

🛡️ Proposed safer implementation
     /// `@brief` Decrement the reference count.
-    void decRef() {
-        if (mNumRefs > 0) {
-            --mNumRefs;
-            if (mNumRefs == 0) {
-                delete this;
-            }
-        }
+    /// `@return` true if the object was deleted, false otherwise.
+    bool decRef() {
+        assert(mNumRefs > 0 && "decRef called on object with zero refcount");
+        --mNumRefs;
+        if (mNumRefs == 0) {
+            delete this;
+            return true;
+        }
+        return false;
     }

Alternatively, for modern C++17, consider using std::shared_ptr with a custom destructor or std::enable_shared_from_this to avoid manual reference counting entirely.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
void decRef() {
if (mNumRefs > 0) {
--mNumRefs;
if (mNumRefs == 0) {
delete this;
}
}
}
/// `@brief` Decrement the reference count.
/// `@return` true if the object was deleted, false otherwise.
bool decRef() {
assert(mNumRefs > 0 && "decRef called on object with zero refcount");
--mNumRefs;
if (mNumRefs == 0) {
delete this;
return true;
}
return false;
}
🧰 Tools
🪛 GitHub Check: SonarCloud Code Analysis

[failure] 355-355: Rewrite the code so that you no longer need this "delete".

See more on https://sonarcloud.io/project/issues?id=kimkulling_tiny_ui&issues=AZzem9QZ5gyFWmdews0l&open=AZzem9QZ5gyFWmdews0l&pullRequest=20

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/tinyui.h` around lines 351 - 358, decRef() currently silently ignores
calls when mNumRefs is 0 and uses delete this, which is unsafe; change decRef to
return a bool and enforce/validate the counter: ensure mNumRefs is initialized
correctly (or assert mNumRefs>0 on entry), decrement mNumRefs and if it reaches
0 perform deletion and return true, otherwise return false; update all callers
of decRef to handle the boolean result so they know the object may have been
destroyed (or, as an alternative, replace manual ref counting with
std::shared_ptr/std::enable_shared_from_this to eliminate delete this and manual
mNumRefs management).

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/widgets.cpp (1)

466-473: ⚠️ Potential issue | 🟠 Major

Inconsistency: progressBar doesn't call incRef() on callback.

Unlike button, imageButton, and treeView, this function doesn't increment the callback's reference count. Since the widget destructor unconditionally calls decRef(), this creates a reference count imbalance that could cause premature deletion or undefined behavior with shared callbacks.

🔧 Proposed fix: add incRef to match other widget functions
     child->mCallback = callback;
+    if (callback != nullptr) {
+        callback->incRef();
+    }
     EventPayload payload;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/widgets.cpp` around lines 466 - 473, The progressBar widget sets
child->mCallback = callback but fails to increment the callback's refcount,
causing imbalance because the widget destructor calls decRef(); fix by calling
callback->incRef() after assigning and before setting callback->mInstance and
pushing it into ctx.mUpdateCallbackList so the lifecycle matches other widgets
like button/imageButton/treeView (i.e., mirror their sequence: assign mCallback,
incRef(), set mInstance, push to mUpdateCallbackList).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/widgets.cpp`:
- Around line 299-303: Before assigning child->mCallback = callback, if
child->mCallback is non-null call child->mCallback->decRef() to release the old
reference, then assign the new callback and call callback->incRef() only if
callback is non-null; i.e., ensure the existing callback is decRef'ed prior to
reassignment (referencing child->mCallback, callback, decRef(), and incRef()) to
avoid the same reference-count leak as in button().
- Around line 273-276: When assigning a new callback to an existing widget you
must release the old reference first to avoid leaking refs: in the code path
where you set child->mCallback (e.g., in createWidget/where mCallback is
reassigned), if child->mCallback is non-null call child->mCallback->decRef()
before overwriting it, then assign child->mCallback = callback and if (callback
!= nullptr) call callback->incRef(); ensure you update every place that
reassigns mCallback so incRef/decRef are balanced.

---

Outside diff comments:
In `@src/widgets.cpp`:
- Around line 466-473: The progressBar widget sets child->mCallback = callback
but fails to increment the callback's refcount, causing imbalance because the
widget destructor calls decRef(); fix by calling callback->incRef() after
assigning and before setting callback->mInstance and pushing it into
ctx.mUpdateCallbackList so the lifecycle matches other widgets like
button/imageButton/treeView (i.e., mirror their sequence: assign mCallback,
incRef(), set mInstance, push to mUpdateCallbackList).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9ab0a756-ee20-4cfb-9586-1c65764d2815

📥 Commits

Reviewing files that changed from the base of the PR and between 55daac0 and 861ca9c.

📒 Files selected for processing (1)
  • src/widgets.cpp

Comment on lines 273 to +276
child->mCallback = callback;
if (callback != nullptr) {
callback->incRef();
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Reference count leak when reassigning callback on existing widget.

createWidget can return an existing widget (lines 119-121). If that widget already has a callback, assigning the new callback overwrites mCallback without calling decRef() on the old one—leaking its reference count.

🔧 Proposed fix: decrement old callback before assignment
+    if (child->mCallback != nullptr) {
+        child->mCallback->decRef();
+    }
     child->mCallback = callback;
     if (callback != nullptr) {
         callback->incRef();
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
child->mCallback = callback;
if (callback != nullptr) {
callback->incRef();
}
if (child->mCallback != nullptr) {
child->mCallback->decRef();
}
child->mCallback = callback;
if (callback != nullptr) {
callback->incRef();
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/widgets.cpp` around lines 273 - 276, When assigning a new callback to an
existing widget you must release the old reference first to avoid leaking refs:
in the code path where you set child->mCallback (e.g., in createWidget/where
mCallback is reassigned), if child->mCallback is non-null call
child->mCallback->decRef() before overwriting it, then assign child->mCallback =
callback and if (callback != nullptr) call callback->incRef(); ensure you update
every place that reassigns mCallback so incRef/decRef are balanced.

Comment on lines 299 to +303
child->mCallback = callback;
if (callback != nullptr) {
callback->incRef();
}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Same reference count leak issue as button().

Apply the same fix here—call decRef() on the existing callback before reassignment.

🔧 Proposed fix
+    if (child->mCallback != nullptr) {
+        child->mCallback->decRef();
+    }
     child->mCallback = callback;
     if (callback != nullptr) {
         callback->incRef();
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/widgets.cpp` around lines 299 - 303, Before assigning child->mCallback =
callback, if child->mCallback is non-null call child->mCallback->decRef() to
release the old reference, then assign the new callback and call
callback->incRef() only if callback is non-null; i.e., ensure the existing
callback is decRef'ed prior to reassignment (referencing child->mCallback,
callback, decRef(), and incRef()) to avoid the same reference-count leak as in
button().

@kimkulling kimkulling merged commit 8bc5248 into main Mar 11, 2026
4 checks passed
@kimkulling kimkulling deleted the bugfix/fix_callback_usage branch March 11, 2026 22:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant