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