Browse Source

putil: Add new primitives for async callbacks/futures

These are a lot lighter than AsyncFuture and safer to use in C++ due to the reliance on RAII.  An async method can now take a CompletionToken, which will implicitly accept an arbitrary callable or an AsyncFuture pointer.
rdb 10 months ago
parent
commit
2ea02ef0c8

+ 11 - 0
panda/src/event/asyncFuture.cxx

@@ -389,6 +389,17 @@ wake_task(AsyncTask *task) {
   }
 }
 
+/**
+ * Internal callback called when a CompletionToken created from this future
+ * completes.
+ */
+void AsyncFuture::
+token_callback(Completable::Data *data, bool success) {
+  AsyncFuture *future = (AsyncFuture *)data;
+  future->set_result(EventParameter(success));
+  unref_delete(future);
+}
+
 /**
  * @see AsyncFuture::gather
  */

+ 32 - 1
panda/src/event/asyncFuture.h

@@ -20,6 +20,7 @@
 #include "eventParameter.h"
 #include "patomic.h"
 #include "small_vector.h"
+#include "completionToken.h"
 
 class AsyncTaskManager;
 class AsyncTask;
@@ -58,7 +59,7 @@ class AsyncTask;
  *
  * @since 1.10.0
  */
-class EXPCL_PANDA_EVENT AsyncFuture : public TypedReferenceCount {
+class EXPCL_PANDA_EVENT AsyncFuture : public TypedReferenceCount, protected Completable::Data {
 PUBLISHED:
   INLINE AsyncFuture();
   virtual ~AsyncFuture();
@@ -109,6 +110,8 @@ public:
 private:
   void wake_task(AsyncTask *task);
 
+  static void token_callback(Completable::Data *, bool success);
+
 protected:
   enum FutureState : patomic_unsigned_lock_free::value_type {
     // Pending states
@@ -136,6 +139,7 @@ protected:
 
   friend class AsyncGatheringFuture;
   friend class AsyncTaskChain;
+  friend class CompletionToken;
   friend class PythonTask;
 
 public:
@@ -199,6 +203,33 @@ private:
   static TypeHandle _type_handle;
 };
 
+#ifndef CPPPARSER
+// Allow passing a future into a method accepting a CompletionToken.
+template<>
+INLINE CompletionToken::
+CompletionToken(AsyncFuture *future) {
+  if (future != nullptr) {
+    future->ref();
+    _callback._data = future;
+    if (_callback._data->_function == nullptr) {
+      _callback._data->_function = &AsyncFuture::token_callback;
+    }
+  }
+}
+
+template<>
+INLINE CompletionToken::
+CompletionToken(PT(AsyncFuture) future) {
+  if (future != nullptr) {
+    _callback._data = future;
+    if (_callback._data->_function == nullptr) {
+      _callback._data->_function = &AsyncFuture::token_callback;
+    }
+    future.cheat() = nullptr;
+  }
+}
+#endif
+
 #include "asyncFuture.I"
 
 #endif // !ASYNCFUTURE_H

+ 4 - 0
panda/src/putil/CMakeLists.txt

@@ -20,6 +20,9 @@ set(P3PUTIL_HEADERS
   clockObject.h clockObject.I
   collideMask.h
   colorSpace.h
+  completable.I completable.h
+  completionCounter.I completionCounter.h
+  completionToken.I completionToken.h
   copyOnWriteObject.h copyOnWriteObject.I
   copyOnWritePointer.h copyOnWritePointer.I
   compareTo.I compareTo.h
@@ -86,6 +89,7 @@ set(P3PUTIL_SOURCES
   callbackObject.cxx
   clockObject.cxx
   colorSpace.cxx
+  completionCounter.cxx
   copyOnWriteObject.cxx
   copyOnWritePointer.cxx
   config_putil.cxx configurable.cxx

+ 61 - 0
panda/src/putil/completable.I

@@ -0,0 +1,61 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file completable.I
+ * @author rdb
+ * @date 2025-01-22
+ */
+
+#ifndef CPPPARSER
+/**
+ *
+ */
+template<class Callable>
+INLINE Completable::
+Completable(Callable callback) :
+  _data(new LambdaData<Callable>(std::move(callback), [](Data *data, bool do_run) {
+    LambdaData<Callable> *self = (LambdaData<Callable> *)data;
+    if (do_run) {
+      std::move(self->_lambda)();
+    }
+    delete self;
+  })) {
+}
+#endif
+
+/**
+ *
+ */
+INLINE Completable::
+Completable(Completable &&from) noexcept :
+  _data(from._data) {
+  from._data = nullptr;
+}
+
+/**
+ *
+ */
+INLINE Completable::
+~Completable() {
+  Data *data = _data;
+  if (data != nullptr) {
+    data->_function.load(std::memory_order_relaxed)(data, false);
+  }
+}
+
+/**
+ *
+ */
+INLINE void Completable::
+operator ()() {
+  Data *data = _data;
+  _data = nullptr;
+  if (data != nullptr) {
+    data->_function.load(std::memory_order_relaxed)(data, true);
+  }
+}

+ 79 - 0
panda/src/putil/completable.h

@@ -0,0 +1,79 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file completable.h
+ * @author rdb
+ * @date 2025-01-22
+ */
+
+#ifndef COMPLETABLE_H
+#define COMPLETABLE_H
+
+#include "pandabase.h"
+#include "patomic.h"
+
+/**
+ * Stores a type-erased callable that is move-only.  May only be called once.
+ */
+class EXPCL_PANDA_PUTIL Completable {
+public:
+  constexpr Completable() = default;
+
+#ifndef CPPPARSER
+  template<class Callable>
+  INLINE Completable(Callable callback);
+#endif
+
+  INLINE Completable(const Completable &copy) = delete;
+  INLINE Completable(Completable &&from) noexcept;
+
+  INLINE void operator ()();
+
+  INLINE ~Completable();
+
+protected:
+  // There are several design approaches here:
+  // 1. Optimize for no data block: do not require dynamic allocation of a data
+  //    block in the simple case where the callback data is only the size of a
+  //    single pointer.  Store two pointers, one function pointer and a data
+  //    pointer(-sized storage), directly on the class here.
+  // 2. Optimize for a data block: store the function pointer on the data block,
+  //    always requiring dynamic allocation.
+  //
+  // Right now I have opted for 2 because it allows the function pointer to be
+  // dynamically swapped (used in CompletionCounter), but this decision may
+  // change in the future.
+
+  struct Data;
+  typedef void CallbackFunction(Data *, bool);
+
+  struct Data {
+    patomic<CallbackFunction *> _function { nullptr };
+  };
+
+  template<typename Lambda>
+  struct LambdaData : public Data {
+    // Must unfortunately be defined inline, since this struct is protected.
+    LambdaData(Lambda lambda, CallbackFunction *function) :
+      _lambda(std::move(lambda)) {
+      _function = function;
+    }
+
+    Lambda _lambda;
+  };
+
+  Data *_data = nullptr;
+
+  friend class AsyncFuture;
+  friend class CompletionCounter;
+  friend class CompletionToken;
+};
+
+#include "completable.I"
+
+#endif

+ 97 - 0
panda/src/putil/completionCounter.I

@@ -0,0 +1,97 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file completionCounter.I
+ * @author rdb
+ * @date 2025-01-22
+ */
+
+/**
+ *
+ */
+INLINE CompletionCounter::
+~CompletionCounter() {
+  CounterData *data = _data;
+  if (data != nullptr) {
+    // then() is not called; we still need something that destructs the data
+    // when done.
+    auto prev_function = data->_function.exchange(&abandon_callback, std::memory_order_relaxed);
+    if (prev_function == nullptr) {
+      // Was already done.
+      delete data;
+    }
+  }
+}
+
+/**
+ * Returns a new token.  May not be called after then().
+ */
+INLINE CompletionToken CompletionCounter::
+make_token() {
+  CompletionToken token;
+  if (_data == nullptr) {
+    _data = new CounterData;
+    _data->_function = &initial_callback;
+  }
+  auto old_value = _data->_counter.fetch_add(1);
+  nassertr(old_value >= 0, token);
+  token._callback._data = _data;
+  return token;
+}
+
+/**
+ * Runs the given callback immediately upon completion.  If the counter is
+ * already done, runs it immediately.  This requires an rvalue because it
+ * consumes the counter, use std::move() if you don't have an rvalue.
+ *
+ * The callback will either be called immediately or directly when the last
+ * token calls complete(), however, it may also be called if a token is
+ * destroyed.  This may happen at unexpected times, such as when the lambda
+ * holding the token is destroyed prematurely.  In this case, however, the
+ * passed success argument will always be false.
+ */
+template<class Callable>
+INLINE void CompletionCounter::
+then(Callable callable) && {
+  // Replace the callback pointer with something that calls the given callable
+  // once the count reaches 0.
+  CounterData *data = _data;
+  nassertv(data != nullptr);
+  _data = nullptr;
+  if (data->_function.load(std::memory_order_acquire) == nullptr) {
+    // Already done.
+    callable((data->_counter.load(std::memory_order_relaxed) & ~0xffff) == 0);
+    delete data;
+    return;
+  }
+
+  static_assert(sizeof(Callable) <= sizeof(data->_storage),
+    "raise storage size in completionCounter.h or reduce lambda captures");
+
+  new (data->_storage) Callable(std::move(callable));
+
+  Completable::CallbackFunction *new_function =
+    [] (Completable::Data *data_ptr, bool success) {
+      CounterData *data = (CounterData *)data_ptr;
+      auto prev_count = data->_counter.fetch_add((success ? 0 : 0x10000) - 1, std::memory_order_release);
+      if ((short)(prev_count & 0xffff) > 1) {
+        return;
+      }
+
+      Callable *callable = (Callable *)data->_storage;
+      std::move(*callable)(success && (prev_count & ~0xffff) == 0);
+      callable->~Callable();
+      delete data;
+    };
+
+  auto prev_function = data->_function.exchange(new_function, std::memory_order_acq_rel);
+  if (UNLIKELY(prev_function == nullptr)) {
+    // Last token finished in the meantime.
+    new_function(data, (data->_counter.load(std::memory_order_relaxed) & ~0xffff) == 0);
+  }
+}

+ 52 - 0
panda/src/putil/completionCounter.cxx

@@ -0,0 +1,52 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file completionCounter.cxx
+ * @author rdb
+ * @date 2025-01-24
+ */
+
+#include "completionCounter.h"
+
+/**
+ * Called when a token is completed before then() is called.
+ */
+void CompletionCounter::
+initial_callback(Completable::Data *data_ptr, bool success) {
+  CounterData &data = *(CounterData *)data_ptr;
+  auto prev_count = data._counter.fetch_add((success ? 0 : 0x10000) - 1, std::memory_order_release);
+  if ((prev_count & 0xffff) == 1) {
+    // We're done early.
+    auto prev_callback = data._function.exchange(nullptr, std::memory_order_acq_rel);
+    nassertv(prev_callback != nullptr);
+
+    // Someone called then() in the meantime.  Call the new callback.  The
+    // refcount will drop below 0 when that's called but they are designed to
+    // handle that.
+    if (prev_callback != &initial_callback) {
+      prev_callback(data_ptr, success && (prev_count & ~0xffff) == 0);
+    }
+  }
+}
+
+/**
+ * Called when a token is completed after this object is destroyed without
+ * then() being called.
+ */
+void CompletionCounter::
+abandon_callback(Completable::Data *data_ptr, bool success) {
+  CounterData &data = *(CounterData *)data_ptr;
+  auto prev_count = data._counter.fetch_sub(1, std::memory_order_relaxed);
+  if ((prev_count & 0xffff) <= 1) {
+    // Done.
+    auto prev_callback = data._function.exchange(nullptr, std::memory_order_relaxed);
+    nassertv(prev_callback != nullptr);
+    nassertv(prev_callback == &abandon_callback);
+    delete &data;
+  }
+}

+ 58 - 0
panda/src/putil/completionCounter.h

@@ -0,0 +1,58 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file completionCounter.h
+ * @author rdb
+ * @date 2025-01-22
+ */
+
+#ifndef COMPLETIONCOUNTER_H
+#define COMPLETIONCOUNTER_H
+
+#include "pandabase.h"
+#include "completionToken.h"
+
+#include <cstddef>
+
+/**
+ * Shared counter that generates "completion tokens" incrementing a counter,
+ * which will decrement the counter once they are finished.  After the tokens
+ * are handed out, a callback may be registered using then(), which will be
+ * called as soon as the last token is done.
+ */
+class EXPCL_PANDA_PUTIL CompletionCounter {
+public:
+  constexpr CompletionCounter() = default;
+  CompletionCounter(const CompletionCounter &copy) = delete;
+
+  INLINE ~CompletionCounter();
+
+  INLINE CompletionToken make_token();
+
+  template<class Callable>
+  INLINE void then(Callable callable) &&;
+
+private:
+  static void initial_callback(Completable::Data *data, bool success);
+  static void abandon_callback(Completable::Data *data, bool success);
+
+protected:
+  struct CounterData : public Completable::Data {
+    // Least significant half is counter, most significant half is error count
+    patomic_signed_lock_free _counter { 0 };
+
+    // Just raise this if the static_assert fires (or limit the size of your
+    // lambda captures).
+    alignas(std::max_align_t) unsigned char _storage[64];
+  };
+  CounterData *_data = nullptr;
+};
+
+#include "completionCounter.I"
+
+#endif

+ 42 - 0
panda/src/putil/completionToken.I

@@ -0,0 +1,42 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file completionToken.I
+ * @author rdb
+ * @date 2025-01-22
+ */
+
+#ifndef CPPPARSER
+/**
+ * Creates a token that calls the given callback when it's done, passing it
+ * true on success and false on failure or abandonment.
+ */
+template<class Callable>
+INLINE CompletionToken::
+CompletionToken(Callable callback) {
+  // Main difference over a Completable is that this will always call the
+  // callback, even on failure, so that cleanup can be done.
+  _callback._data = new Completable::LambdaData<Callable>(std::move(callback), [](Completable::Data *data, bool success) {
+    Completable::LambdaData<Callable> *self = (Completable::LambdaData<Callable> *)data;
+    std::move(self->_lambda)(success);
+    delete self;
+  });
+}
+#endif
+
+/**
+ *
+ */
+INLINE void CompletionToken::
+complete(bool success) {
+  Completable::Data *data = _callback._data;
+  if (data != nullptr) {
+    _callback._data = nullptr;
+    data->_function.load(std::memory_order_relaxed)(data, success);
+  }
+}

+ 56 - 0
panda/src/putil/completionToken.h

@@ -0,0 +1,56 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file completionToken.h
+ * @author rdb
+ * @date 2025-01-22
+ */
+
+#ifndef COMPLETIONTOKEN_H
+#define COMPLETIONTOKEN_H
+
+#include "pandabase.h"
+#include "pnotify.h"
+#include "completable.h"
+
+/**
+ * A completion token can be created from a callback, future or
+ * CompletionCounter and can be passed into an asynchronous operation in order
+ * to receive a signal when it is done.
+ *
+ * The asynchronous operation should call complete() on it when it is done,
+ * with a boolean value indicating success or failure.  If the token is
+ * destroyed prematurely, it is treated as if it called complete(false).
+ *
+ * This should be preferred over passing an AsyncFuture into a method since
+ * a CompletionToken provides both more flexibility in use (due to accepting
+ * an arbitrary callback) and more safety (since the RAII semantics guarantees
+ * that the callback is never silently dropped).
+ *
+ * The token may only be moved, not copied.
+ */
+class EXPCL_PANDA_PUTIL CompletionToken {
+public:
+  constexpr CompletionToken() = default;
+
+#ifndef CPPPARSER
+  template<class Callable>
+  INLINE CompletionToken(Callable callback);
+#endif
+
+  void complete(bool success);
+
+protected:
+  Completable _callback;
+
+  friend class CompletionCounter;
+};
+
+#include "completionToken.I"
+
+#endif

+ 1 - 0
panda/src/putil/p3putil_composite1.cxx

@@ -17,6 +17,7 @@
 #include "callbackObject.cxx"
 #include "clockObject.cxx"
 #include "colorSpace.cxx"
+#include "completionCounter.cxx"
 #include "config_putil.cxx"
 #include "configurable.cxx"
 #include "copyOnWriteObject.cxx"