blob: 335c86264552911dcf529a25e951cc9022cd6731 [file] [log] [blame] [edit]
// Copyright 2023 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef LIB_ASYNC_PATTERNS_CPP_SENDABLE_H_
#define LIB_ASYNC_PATTERNS_CPP_SENDABLE_H_
#include <lib/async_patterns/cpp/internal/sendable.h>
#include <lib/stdcompat/type_traits.h>
#include <tuple>
#include <type_traits>
namespace async_patterns {
// |BindForSending| function packages up |args| so that they may be later
// executed as arguments to |callable| in a different execution context,
// asynchronously and possibly concurrently. This is called "sending". For
// example, |DispatcherBound| uses |BindForSending| to send arguments to the
// object owned by |DispatcherBound|, as part of asynchronously calling their
// member functions.
//
// It is similar to |std::bind_front|, but will first forward the |args| into
// the capture state of the resulting lambda, and then move the captured args
// into |callable| when invoked later.
//
// ## Safety of sending arguments
//
// Because the |args| are used at a later time, one needs to be aware of risks
// of use-after-free and dace races. There are also the following restrictions:
//
// - Value types may be sent via copying or moving. Whether one passes an |Arg|,
// |const Arg&|, or |Arg&&| etc on the sending side, the |callable| will have
// its own uniquely owned instance that they may safely manipulate.
//
// - Move-only types such as |std::unique_ptr<Arg>| may be sent via |std::move|.
//
// - The |callable| may not take raw pointer or non-const reference arguments.
// Sending those may result in use-after-frees when the callable uses a
// pointee asynchronously after an unspecified amount of time. It's not worth
// the memory safety risks to support the cases where a pointee outlives the
// dispatcher that is running tasks for |T|.
//
// - If the |callable| takes |const Arg&| as an argument, one may send an
// instance of |Arg| via copying or moving. The instance will live until the
// callable finishes executing.
//
// - When sending a ref-counted pointer such as |std::shared_ptr<Arg>|, both the
// sender and the |callable| will be able to concurrently access |Arg|. Be
// sure to add appropriate synchronization to |Arg|, for example protecting
// fields with mutexes. This pattern is called "shared-state concurrency".
//
// - These checks are performed for each argument, but there could still be raw
// pointers or references inside an object that is sent. One must ensure that
// the pointee remain alive until the asynchronous tasks are run, possibly
// indefinitely until the dispatcher is destroyed.
//
template <typename Callable, typename... Args>
auto BindForSending(Callable callable, Args&&... args) {
// An individual |Arg| in |Args| is either an l-value reference or an r-value
// reference, due to universal reference rules.
//
// Therefore, this check ensures the callable matches its arguments. If the
// user passes an l-value to a move-only argument, the |is_invocable| will
// fail, because it will try to invoke a function taking |Arg| with |Arg&|.
static_assert(std::is_invocable_v<Callable, Args...>,
"The |Callable| function must be invocable with argument |Args|. "
"Check that the arguments provided are correct. "
"For example, did you call a function that consumes a move-only type |Foo| "
"with an argument type of |Foo&| instead of |Foo&&|?");
// We package the arguments into a tuple because capturing a template
// parameter pack is a C++20 feature.
//
// |make_tuple| moves |Arg&&| into |Arg|, and copies |const Arg&| into |Arg|.
// c.f. https://en.cppreference.com/w/cpp/utility/tuple/make_tuple
// The behavior of |make_tuple| w.r.t. |Arg&| is not relevant here since we
// disallow sending non-const references.
return [callable = std::move(callable),
args = std::make_tuple(std::forward<Args>(args)...)]() mutable {
// The |std::move| inside prevents |callable| from taking |Arg&| arguments.
// Unfortunately, unlike pointer arguments, there is no good way to reject
// non-const l-value references, because the universal reference rules will
// make |Arg| an l-value reference even when the user provided |Arg| by
// value. We should instead check that the underlying |callable| don't
// _also_ accept an l-value reference -- library code using |BindForSending|
// should use |internal::CheckArguments| on the relevant member function.
// Here, as an extra safety measure, we always move each |arg| into the
// call. A non-const l-value reference will not be able to bind to a
// non-const r-value reference.
//
// ** NOTE TO USERS ** If you get a compiler error pointing to this
// vicinity, check the documentation comments of |BindForSending|.
// Passing mutable references and raw pointers are not allowed.
return std::apply(
[callable = std::move(callable)](auto&&... args) mutable {
constexpr auto check_arg = [](auto&& arg) {
using Arg = decltype(arg);
static_assert(!std::is_pointer_v<cpp20::remove_cvref_t<Arg>>,
"Sending raw pointers is not allowed. "
"See class documentation comments of |BindForSending|.");
return true;
};
(void)(check_arg(args) && ...);
return callable(internal::UnSmuggle(std::move(args))...);
},
args);
};
}
namespace internal {
template <typename Arg>
constexpr void CheckOneArg() {
static_assert(!std::is_lvalue_reference_v<Arg> || std::is_const_v<std::remove_reference_t<Arg>>,
"Sending non-const l-value references is not allowed. "
"See class documentation comments of |BindForSending|.");
}
template <typename... Args>
struct CheckArguments {
static constexpr void Check() {}
};
template <typename Arg, typename... Rest>
struct CheckArguments<Arg, Rest...> {
static constexpr void Check() {
CheckOneArg<Arg>();
CheckArguments<Rest...>::Check();
}
};
} // namespace internal
} // namespace async_patterns
#endif // LIB_ASYNC_PATTERNS_CPP_SENDABLE_H_