| // 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 |