blob: bbf7fbd09d5ca37ba3f3add57d84db110f9b7996 [file] [log] [blame] [edit]
// Copyright Microsoft and CHERIoT Contributors.
// SPDX-License-Identifier: MIT
#pragma once
#include <cdefs.h>
/**
* The type of a thread-pool callback. This is a CHERI callback so that it can
* run in the compartment that schedule the work to run, but a different
* thread.
*/
typedef __cheri_callback void (*ThreadPoolCallback)(void *);
/**
* Message to a thread pool. This encapsulates a simple closure that will be
* invoked in the next available thread in a thread pool.
*/
struct ThreadPoolMessage
{
/**
* The function pointer that will be called in the thread pool.
*/
ThreadPoolCallback invoke;
/**
* The data associated with the asynchronous invocation.
*/
void *data;
};
__BEGIN_DECLS
/**
* Invoke a function that takes a `void*` argument in another thread. The
* function will be invoked with `data` as the argument. The `data` argument
* must be sealed.
*
* This can block indefinitely until the thread pool is able to process a
* message.
*
* FIXME: We should add a non-blocking variant of this.
*
* Returns 0 on success, -EINVAL if either of the arguments are invalid.
*/
int __cheri_compartment("thread_pool")
thread_pool_async(ThreadPoolCallback fn, void *data);
/**
* Run a thread pool. This does not return and can be used as a thread entry
* point.
*/
void __cheri_compartment("thread_pool") thread_pool_run(void);
__END_DECLS
#ifdef __cplusplus
# include <memory>
# include <stdlib.h>
# include <token.h>
# include <type_traits>
# include <cheri.hh>
/**
* For C++ programmers, we provide some more user-friendly wrappers.
*/
namespace thread_pool
{
/**
* Implementation details, should not be used outside of this header.
*/
namespace detail
{
/**
* Helper that generates a different sealing key per type using the
* allocator's token mechanism.
*/
template<typename T>
inline SKey sealing_key_for_type()
{
static SKey key = token_key_new();
return key;
}
/**
* Helper that provides a callback function for invoking and then
* freeing a sealed heap-allocated lambda. The lambda is sealed with
* the software-defined sealing key constructed by the
* `sealing_key_for_type` helper.
*/
template<typename T>
__cheri_callback void wrap_callback_lambda(void *rawFn)
{
auto key = sealing_key_for_type<T>();
auto fn = token_unseal(key, Sealed(static_cast<T *>(rawFn)));
if (fn == nullptr)
{
return;
}
(*fn)();
token_obj_destroy(MALLOC_CAPABILITY, key, static_cast<SObj>(rawFn));
}
/**
* Helper to wrap pure C function (including a stateless lambda) as a
* callback.
*/
template<typename Fn>
__cheri_callback void wrap_callback_function(void *)
{
// C++ objects are guaranteed to have unique addresses and so a
// zero-sized object is actually 1 byte and using `sizeof(Fn) == 1`
// would not let us tell the difference between empty lambdas and
// ones with a one-byte capture. To ensure that this object is
// really stateless, we make it a field of another type, marking it
// as not requiring a unique address, and then check that the
// offset of the following field is 0.
struct Test
{
// The stateless callable object
[[no_unique_address]] Fn f;
// An field that can share the same address as `f` if `f` is
// truly stateless.
char b;
};
static_assert(offsetof(Test, b) == 0,
"Stateless callable object is not stateless");
// Invoke an empty instance of this stateless callable object.
Fn{}();
}
} // namespace detail
/**
* Asynchronously invoke a lambda. This moves the lambda to the heap and
* passes it to the thread pool's queue. If the lambda copies any stack
* objects by reference then the copy will fail.
*/
template<typename T>
void async(T &&lambda)
{
// If this is a stateless function, just send a callback function
// pointer, don't copy zero bytes of state to the heap.
if constexpr (std::is_convertible_v<T, void (*)(void)>)
{
thread_pool_async(
&detail::wrap_callback_function<std::remove_cvref_t<T>>, nullptr);
}
else
{
// If this is a stateful lambda, move it to the heap, create a
// callback function that will invoke it and free it, and send a
// pointer to the wrapper and the heap-allocated object.
void *buffer;
using LambdaType = std::remove_reference_t<decltype(lambda)>;
// Allocate a new sealed object with a key that is unique to this
// type.
Timeout t{UnlimitedTimeout};
void *sealed = token_sealed_unsealed_alloc(
&t,
MALLOC_CAPABILITY,
detail::sealing_key_for_type<LambdaType>(),
sizeof(lambda),
&buffer);
// Copy the lambda into the new allocation.
// Note: We silence a warning here because we *do* want to
// explicitly move, not forward.
T *copy = new (buffer) T(
std::move(lambda)); // NOLINT(bugprone-move-forwarding-reference)
// Create the wrapper that will unseal and invoke the lambda.
ThreadPoolCallback invoke =
&detail::wrap_callback_lambda<LambdaType>;
// Dispatch it.
thread_pool_async(invoke, sealed);
}
}
} // namespace thread_pool
#endif