pw_intrusive_ptr: Add Recyclable
Port fbl::Recyclable (from Fuchsia) to pw_intrusive_ptr.
Bug: fxb/100658
Change-Id: Ieb8927cd8869a1fe621ed2275e810cb75f57efdf
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/126782
Commit-Queue: Ben Lawson <benlawson@google.com>
Pigweed-Auto-Submit: Ben Lawson <benlawson@google.com>
Reviewed-by: Wyatt Hepler <hepler@google.com>
diff --git a/pw_intrusive_ptr/BUILD.bazel b/pw_intrusive_ptr/BUILD.bazel
index a88643b..257c2a7 100644
--- a/pw_intrusive_ptr/BUILD.bazel
+++ b/pw_intrusive_ptr/BUILD.bazel
@@ -30,11 +30,32 @@
"public/pw_intrusive_ptr/intrusive_ptr.h",
],
includes = ["public"],
- deps = ["//pw_assert"],
+ deps = [
+ ":pw_recyclable",
+ "//pw_assert",
+ ],
+)
+
+pw_cc_library(
+ name = "pw_recyclable",
+ hdrs = [
+ "public/pw_intrusive_ptr/recyclable.h",
+ ],
+ includes = ["public"],
)
pw_cc_test(
name = "intrusive_ptr_test",
- srcs = ["intrusive_ptr_test.cc"],
+ srcs = [
+ "intrusive_ptr_test.cc",
+ ],
+ deps = [":pw_intrusive_ptr"],
+)
+
+pw_cc_test(
+ name = "recyclable_test",
+ srcs = [
+ "recyclable_test.cc",
+ ],
deps = [":pw_intrusive_ptr"],
)
diff --git a/pw_intrusive_ptr/BUILD.gn b/pw_intrusive_ptr/BUILD.gn
index 2d4938b..fee72c0 100644
--- a/pw_intrusive_ptr/BUILD.gn
+++ b/pw_intrusive_ptr/BUILD.gn
@@ -31,6 +31,12 @@
]
sources = [ "ref_counted_base.cc" ]
deps = [ "$dir_pw_assert" ]
+ public_deps = [ ":pw_recyclable" ]
+}
+
+pw_source_set("pw_recyclable") {
+ public_configs = [ ":public_include_path" ]
+ public = [ "public/pw_intrusive_ptr/recyclable.h" ]
}
pw_doc_group("docs") {
@@ -48,3 +54,11 @@
# TODO(b/260624583): Fix this for //targets/rp2040
enable_if = pw_build_EXECUTABLE_TARGET_TYPE != "pico_executable"
}
+
+pw_test("recyclable_test") {
+ sources = [ "recyclable_test.cc" ]
+ deps = [ ":pw_intrusive_ptr" ]
+
+ # TODO(b/260624583): Fix this for //targets/rp2040
+ enable_if = pw_build_EXECUTABLE_TARGET_TYPE != "pico_executable"
+}
diff --git a/pw_intrusive_ptr/docs.rst b/pw_intrusive_ptr/docs.rst
index 33d307a..59295b7 100644
--- a/pw_intrusive_ptr/docs.rst
+++ b/pw_intrusive_ptr/docs.rst
@@ -4,6 +4,8 @@
pw_intrusive_ptr
----------------
+IntrusivePtr
+------------
``pw::IntrusivePtr`` is a smart shared pointer that relies on the pointed-at
object to do the reference counting. Its API is based on ``std::shared_ptr`` but
requires the pointed-at class to provide ``AddRef()`` and ``ReleaseRef()``
@@ -53,3 +55,29 @@
it can be returned by const reference is the trivial getter for the object
field. When returning locally created ``IntrusivePtr`` or a pointer that was
casted to the base class it MUST be returned by value.
+
+Recyclable
+----------
+``pw::Recyclable`` is a mixin that can be used with supported smart pointers
+like ``pw::IntrusivePtr`` to specify a custom memory cleanup routine instead
+of `delete`. The cleanup routine is specified as a method with the signature
+``void pw_recycle()``. For example:
+
+.. code-block:: cpp
+
+ class Foo : public pw::Recyclable<Foo>, public pw::IntrusivePtr<Foo> {
+ public:
+ // public implementation here
+ private:
+ friend class pw::Recyclable<Foo>;
+ void pw_recycle() {
+ if (should_recycle())) {
+ do_recycle_stuff();
+ } else {
+ delete this;
+ }
+ }
+ };
+
+``Recyclable`` can be used to avoid heap allocation when using smart pointers,
+as the recycle routine can return memory to a memory pool.
diff --git a/pw_intrusive_ptr/public/pw_intrusive_ptr/intrusive_ptr.h b/pw_intrusive_ptr/public/pw_intrusive_ptr/intrusive_ptr.h
index 0b86230..9a68868 100644
--- a/pw_intrusive_ptr/public/pw_intrusive_ptr/intrusive_ptr.h
+++ b/pw_intrusive_ptr/public/pw_intrusive_ptr/intrusive_ptr.h
@@ -18,6 +18,7 @@
#include <utility>
#include "pw_intrusive_ptr/internal/ref_counted_base.h"
+#include "pw_intrusive_ptr/recyclable.h"
namespace pw {
@@ -55,9 +56,10 @@
// pointer should be done through IntrusivePtr after the wrapping or while at
// least one IntrusivePtr object owning it is in scope.
//
- // Only heap-allocated pointers should be used with IntrusivePtr. An attempt
- // to wrap the stack-allocated object with the IntrusivePtr will result in a
- // crash on the destruction.
+ // IntrusivePtr can be used with either heap-allocated pointers or
+ // stack/static allocated objects if T is Recyclable. An attempt to wrap a
+ // stack-allocated object with a non-Recyclable IntrusivePtr will result in a
+ // crash on destruction.
explicit IntrusivePtr(T* p) : ptr_(p) {
if (ptr_) {
ptr_->AddRef();
@@ -100,7 +102,7 @@
~IntrusivePtr() {
if (ptr_ && ptr_->ReleaseRef()) {
- delete ptr_;
+ recycle_or_delete(ptr_);
}
}
@@ -131,6 +133,15 @@
"virtual destructor or T == const U.");
}
+ // Support Ts that inherit from the Recyclable mixin.
+ static void recycle_or_delete(T* ptr) {
+ if constexpr (::pw::internal::has_pw_recycle_v<T>) {
+ ::pw::internal::recycle<T>(ptr);
+ } else {
+ delete ptr;
+ }
+ }
+
T* ptr_;
};
diff --git a/pw_intrusive_ptr/public/pw_intrusive_ptr/recyclable.h b/pw_intrusive_ptr/public/pw_intrusive_ptr/recyclable.h
new file mode 100644
index 0000000..4c339c0
--- /dev/null
+++ b/pw_intrusive_ptr/public/pw_intrusive_ptr/recyclable.h
@@ -0,0 +1,135 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include <type_traits>
+
+// pw::Recyclable<T>
+//
+// Notes:
+//
+// pw::Recyclable<T> is a mix-in class which allows users to control what
+// happens to objects when they reach the end of their lifecycle, as determined
+// by the Pigweed managed pointer classes.
+//
+// The general idea is as follows. A developer might have some sort of factory
+// pattern where they hand out unique_ptr<>s or IntrusivePtr<>s to objects which
+// they have created. When their user is done with the object and the managed
+// pointers let go of it, instead of executing the destructor and deleting the
+// object, the developer may want to "recycle" the object and use it for some
+// internal purpose. Examples include...
+//
+// 1) Putting the object on some sort of internal list to hand out again
+// of the object is re-usable and the cost of construction/destruction
+// is high.
+// 2) Putting the object into some form of deferred destruction queue
+// because users are either too high priority to pay the cost of
+// destruction when the object is released, or because the act of
+// destruction might involve operations which are not permitted when
+// the object is released (perhaps the object is released at IRQ time,
+// but the system needs to be running in a thread in order to properly
+// clean up the object)
+// 3) Re-using the object internally for something like bookkeeping
+// purposes.
+//
+// In order to make use of the feature, users need to do two things.
+//
+// 1) Derive from pw::Recyclable<T>.
+// 2) Implement a method with the signature "void pw_recycle()"
+//
+// When deriving from Recyclable<T>, T should be devoid of cv-qualifiers (even
+// if the managed pointers handed out by the user's code are const or volatile).
+// In addition, pw_recycle must be visible to pw::Recyclable<T>, either
+// because it is public or because the T is friends with pw::Recyclable<T>.
+//
+// :: Example ::
+//
+// Some code hands out unique pointers to const Foo objects and wishes to
+// have the chance to recycle them. The code would look something like
+// this...
+//
+// class Foo : public pw::Recyclable<Foo> {
+// public:
+// // public implementation here
+// private:
+// friend class pw::Recyclable<Foo>;
+// void pw_recycle() {
+// if (should_recycle())) {
+// do_recycle_stuff();
+// } else {
+// delete this;
+// }
+// }
+// };
+//
+// Note: the intention is to use this feature with managed pointers,
+// which will automatically detect and call the recycle method if
+// present. That said, there is nothing to stop users for manually
+// calling pw_recycle, provided that it is visible to the code which
+// needs to call it.
+
+namespace pw {
+
+// Default implementation of pw::Recyclable.
+//
+// Note: we provide a default implementation instead of just a fwd declaration
+// so we can add a static_assert which will give a user a more human readable
+// error in case they make the mistake of deriving from pw::Recyclable<const
+// Foo> instead of pw::Recyclable<Foo>
+template <typename T, typename = void>
+class Recyclable {
+ // Note: static assert must depend on T in order to trigger only when the
+ // template gets expanded. If it does not depend on any template parameters,
+ // eg static_assert(false), then it will always explode, regardless of whether
+ // or not the template is ever expanded.
+ static_assert(
+ std::is_same_v<T, T> == false,
+ "pw::Recyclable<T> objects must not specify cv-qualifiers for T. "
+ "Derive from pw::Recyclable<Foo>, not pw::Recyclable<const Foo>");
+};
+
+namespace internal {
+
+// Test to see if an object is recyclable. An object of type T is considered to
+// be recyclable if it derives from pw::Recyclable<T>
+template <typename T>
+inline constexpr bool has_pw_recycle_v =
+ std::is_base_of_v<::pw::Recyclable<std::remove_cv_t<T>>, T>;
+
+template <typename T>
+inline void recycle(T* ptr) {
+ static_assert(has_pw_recycle_v<T>, "T must derive from pw::Recyclable");
+ Recyclable<std::remove_cv_t<T>>::pw_recycle_thunk(
+ const_cast<std::remove_cv_t<T>*>(ptr));
+}
+
+} // namespace internal
+
+template <typename T>
+class Recyclable<T, std::enable_if_t<std::is_same_v<std::remove_cv_t<T>, T>>> {
+ private:
+ friend void ::pw::internal::recycle<T>(T*);
+ friend void ::pw::internal::recycle<const T>(const T*);
+
+ static void pw_recycle_thunk(T* ptr) {
+ static_assert(std::is_same_v<decltype(&T::pw_recycle), void (T::*)(void)>,
+ "pw_recycle() methods must be non-static member functions "
+ "with the signature 'void pw_recycle()', and be visible to "
+ "pw::Recyclable<T> (either because they are public, or "
+ "because of friendship).");
+ ptr->pw_recycle();
+ }
+};
+
+} // namespace pw
diff --git a/pw_intrusive_ptr/recyclable_test.cc b/pw_intrusive_ptr/recyclable_test.cc
new file mode 100644
index 0000000..9069d62
--- /dev/null
+++ b/pw_intrusive_ptr/recyclable_test.cc
@@ -0,0 +1,89 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_intrusive_ptr/recyclable.h"
+
+#include <stdint.h>
+
+#include <utility>
+
+#include "gtest/gtest.h"
+#include "pw_intrusive_ptr/intrusive_ptr.h"
+
+namespace pw {
+namespace {
+
+class TestItem : public RefCounted<TestItem>, public Recyclable<TestItem> {
+ public:
+ TestItem() = default;
+
+ virtual ~TestItem() {}
+
+ inline static int32_t recycle_counter = 0;
+
+ private:
+ friend class Recyclable<TestItem>;
+ void pw_recycle() {
+ recycle_counter++;
+ delete this;
+ }
+};
+
+// Class that thas the pw_recyclable method, but does not derive from
+// Recyclable.
+class TestItemNonRecyclable : public RefCounted<TestItemNonRecyclable> {
+ public:
+ TestItemNonRecyclable() = default;
+
+ virtual ~TestItemNonRecyclable() {}
+
+ inline static int32_t recycle_counter = 0;
+
+ void pw_recycle() { recycle_counter++; }
+};
+
+class RecyclableTest : public ::testing::Test {
+ protected:
+ void SetUp() override {
+ TestItem::recycle_counter = 0;
+ TestItemNonRecyclable::recycle_counter = 0;
+ }
+};
+
+TEST_F(RecyclableTest, DeletingLastPtrRecyclesTheObject) {
+ {
+ IntrusivePtr<TestItem> ptr(new TestItem());
+ EXPECT_EQ(TestItem::recycle_counter, 0);
+ }
+ EXPECT_EQ(TestItem::recycle_counter, 1);
+}
+
+TEST_F(RecyclableTest, ConstRecycle) {
+ {
+ IntrusivePtr<const TestItem> ptr(new TestItem());
+ EXPECT_EQ(TestItem::recycle_counter, 0);
+ }
+ EXPECT_EQ(TestItem::recycle_counter, 1);
+}
+
+TEST_F(RecyclableTest, NonRecyclableWithPwRecycleMethod) {
+ {
+ IntrusivePtr<TestItemNonRecyclable> ptr(new TestItemNonRecyclable());
+ EXPECT_EQ(TestItemNonRecyclable::recycle_counter, 0);
+ }
+ EXPECT_EQ(TestItemNonRecyclable::recycle_counter, 0);
+}
+
+} // namespace
+} // namespace pw