Spicy
fiber.h
1 // Copyright (c) 2020-2021 by the Zeek Project. See LICENSE for details.
2 
3 #pragma once
4 
5 #include <csetjmp>
6 #include <functional>
7 #include <iostream>
8 #include <memory>
9 #include <optional>
10 #include <string>
11 #include <type_traits>
12 #include <utility>
13 #include <vector>
14 
15 #include <hilti/rt/any.h>
16 #include <hilti/rt/autogen/config.h>
17 #include <hilti/rt/exception.h>
18 #include <hilti/rt/types/reference.h>
19 #include <hilti/rt/util.h>
20 
21 struct Fiber;
22 
23 // Fiber entry point for execution of fiber payload.
24 extern "C" void __fiber_run_trampoline(void* args);
25 
26 // Fiber entry point for stack switch trampoline.
27 extern "C" void __fiber_switch_trampoline(void* args);
28 
29 namespace hilti::rt {
30 
31 namespace detail {
32 class Fiber;
33 } // namespace detail
34 
35 namespace resumable {
37 using Handle = detail::Fiber;
38 } // namespace resumable
39 
40 namespace detail {
41 
43 struct FiberContext {
44  FiberContext();
45  ~FiberContext();
46 
48  std::unique_ptr<detail::Fiber> main;
49 
51  std::unique_ptr<detail::Fiber> switch_trampoline;
52 
54  detail::Fiber* current = nullptr;
55 
57  std::unique_ptr<::Fiber> shared_stack;
58 
60  std::vector<std::unique_ptr<Fiber>> cache;
61 };
62 
66 struct StackBuffer {
72  StackBuffer(const ::Fiber* fiber) : _fiber(fiber) {}
73 
75  ~StackBuffer();
76 
82  std::pair<char*, char*> activeRegion() const;
83 
88  std::pair<char*, char*> allocatedRegion() const;
89 
95  size_t activeSize() const { return static_cast<size_t>(activeRegion().second - activeRegion().first); }
96 
98  size_t allocatedSize() const { return static_cast<size_t>(allocatedRegion().second - allocatedRegion().first); }
99 
101  size_t liveRemainingSize() const;
102 
104  void save();
105 
110  void restore() const;
111 
112 private:
113  const ::Fiber* _fiber;
114  void* _buffer = nullptr; // allocated memory holding swapped out stack content
115 };
116 
117 // Render stack region for use in debug output.
118 inline std::ostream& operator<<(std::ostream& out, const StackBuffer& s) {
119  out << fmt("%p-%p:%zu", s.activeRegion().first, s.activeRegion().second, s.activeSize());
120  return out;
121 }
122 
124 class Callback {
125 public:
126  template<typename F>
127  Callback(F f)
128  : _f(std::move(f)), _invoke([](const hilti::rt::any& f, resumable::Handle* h) -> hilti::rt::any {
129  return hilti::rt::any_cast<F>(f)(h);
130  }) {}
131 
132  Callback(const Callback&) = default;
133  Callback(Callback&&) = default;
134 
135  Callback& operator=(const Callback&) = default;
136  Callback& operator=(Callback&&) = default;
137 
138  hilti::rt::any operator()(resumable::Handle* h) const { return _invoke(_f, h); }
139 
140 private:
141  hilti::rt::any _f; //< Type-erased storage for the concrete callback.
142  hilti::rt::any (*_invoke)(const hilti::rt::any& f, resumable::Handle* h); //< Invoke type-erased callback.
143 };
144 
151 class Fiber {
152 public:
154  enum class Type : int64_t {
155  IndividualStack,
156  SharedStack,
158  Main,
159  SwitchTrampoline,
160  };
161 
162  Fiber(Type type);
163  ~Fiber();
164 
165  Fiber(const Fiber&) = delete;
166  Fiber(Fiber&&) = delete;
167  Fiber& operator=(const Fiber&) = delete;
168  Fiber& operator=(Fiber&&) = delete;
169 
170  void init(Callback f) {
171  _result = {};
172  _exception = nullptr;
173  _function = std::move(f);
174  }
175 
177  auto type() { return _type; }
178 
180  const auto& stackBuffer() const { return _stack_buffer; }
181 
182  void run();
183  void yield();
184  void resume();
185  void abort();
186 
187  bool isMain() const { return _type == Type::Main; }
188 
189  bool isDone() {
190  switch ( _state ) {
191  case State::Running:
192  case State::Yielded: return false;
193 
194  case State::Aborting:
195  case State::Finished:
196  case State::Idle:
197  case State::Init:
198  // All these mean we didn't recently run a function that could have
199  // produced a result still pending.
200  return true;
201  }
202  cannot_be_reached(); // For you, GCC.
203  }
204 
205  auto&& result() { return std::move(_result); }
206  std::exception_ptr exception() const { return _exception; }
207 
208  std::string tag() const;
209 
210  static std::unique_ptr<Fiber> create();
211  static void destroy(std::unique_ptr<Fiber> f);
212  static void primeCache();
213  static void reset();
214 
215  struct Statistics {
216  uint64_t total;
217  uint64_t current;
218  uint64_t cached;
219  uint64_t max;
220  uint64_t initialized;
221  };
222 
223  static Statistics statistics();
224 
225 private:
226  friend void ::__fiber_run_trampoline(void* argsp);
227  friend void ::__fiber_switch_trampoline(void* argsp);
228 
229  enum class State { Init, Running, Aborting, Yielded, Idle, Finished };
230 
231  void _yield(const char* tag);
232  void _activate(const char* tag);
233 
235  static void _startSwitchFiber(const char* tag, detail::Fiber* to);
236 
238  static void _finishSwitchFiber(const char* tag);
239 
241  static void _executeSwitch(const char* tag, detail::Fiber* from, detail::Fiber* to);
242 
243  Type _type;
244  State _state{State::Init};
245  std::optional<Callback> _function;
246  std::optional<hilti::rt::any> _result;
247  std::exception_ptr _exception;
248 
250  std::unique_ptr<::Fiber> _fiber;
251 
253  Fiber* _caller = nullptr;
254 
256  StackBuffer _stack_buffer;
257 
258 #ifdef HILTI_HAVE_ASAN
259 
260  struct {
261  const void* stack = nullptr;
262  size_t stack_size = 0;
263  void* fake_stack = nullptr;
264  } _asan;
265 #endif
266 
267  // TODO: Usage of these isn't thread-safe. Should become "atomic" and
268  // move into global state.
269  inline static uint64_t _total_fibers;
270  inline static uint64_t _current_fibers;
271  inline static uint64_t _cached_fibers;
272  inline static uint64_t _max_fibers;
273  inline static uint64_t _initialized; // number of trampolines run
274 };
275 
276 std::ostream& operator<<(std::ostream& out, const Fiber& fiber);
277 
278 extern void yield();
279 
286 extern void checkStack();
287 
288 } // namespace detail
289 
295 class Resumable {
296 public:
303  template<typename Function, typename = std::enable_if_t<std::is_invocable<Function, resumable::Handle*>::value>>
304  Resumable(Function f) : _fiber(detail::Fiber::create()) {
305  _fiber->init(std::move(f));
306  }
307 
308  Resumable() = default;
309  Resumable(const Resumable& r) = delete;
310  Resumable(Resumable&& r) noexcept = default;
311  Resumable& operator=(const Resumable& other) = delete;
312  Resumable& operator=(Resumable&& other) noexcept = default;
313 
314  ~Resumable() {
315  if ( _fiber )
316  detail::Fiber::destroy(std::move(_fiber));
317  }
318 
320  void run();
321 
323  void resume();
324 
326  void abort();
327 
329  resumable::Handle* handle() { return _fiber.get(); }
330 
335  bool hasResult() const { return _done && _result.has_value(); }
336 
341  template<typename Result>
342  const Result& get() const {
343  assert(static_cast<bool>(_result));
344 
345  if constexpr ( std::is_same<Result, void>::value )
346  return {};
347  else {
348  try {
349  return hilti::rt::any_cast<const Result&>(*_result);
350  } catch ( const hilti::rt::bad_any_cast& ) {
351  throw InvalidArgument("mismatch in result type");
352  }
353  }
354  }
355 
357  explicit operator bool() const { return _done; }
358 
359 private:
360  void yielded();
361 
362  void checkFiber(const char* location) const {
363  if ( ! _fiber )
364  throw std::logic_error(std::string("fiber not set in ") + location);
365  }
366 
367  std::unique_ptr<detail::Fiber> _fiber;
368  bool _done = false;
369  std::optional<hilti::rt::any> _result;
370 };
371 
372 namespace resumable::detail {
373 
375 template<typename T>
376 auto copyArg(T t) {
377  // In general, we can't move references to the heap.
378  static_assert(! std::is_reference<T>::value, "copyArg() does not accept references other than ValueReference<T>.");
379  return t;
380 }
381 
382 // Special case: We don't want to (nor need to) deep-copy value references.
383 // Their payload already resides on the heap, so reuse that.
384 template<typename T>
385 ValueReference<T> copyArg(const ValueReference<T>& t) {
386  return ValueReference<T>(t.asSharedPtr());
387 }
388 
389 // Special case: We don't want to (nor need to) deep-copy value references.
390 // Their payload already resides on the heap, so reuse that.
391 template<typename T>
393  return ValueReference<T>(t.asSharedPtr());
394 }
395 
396 } // namespace resumable::detail
397 
398 namespace fiber {
399 
404 template<typename Function>
405 auto execute(Function f) {
406  Resumable r(std::move(f));
407  r.run();
408  return r;
409 }
410 
411 } // namespace fiber
412 } // namespace hilti::rt
void init()
Definition: init.cc:22
std::shared_ptr< T > asSharedPtr() const
Definition: reference.h:121
std::unique_ptr<::Fiber > shared_stack
Definition: fiber.h:57
size_t activeSize() const
Definition: fiber.h:95
Definition: any.h:7
Definition: fiber.h:124
StackBuffer(const ::Fiber *fiber)
Definition: fiber.h:72
Type
Definition: fiber.h:154
Definition: fiber.h:43
void cannot_be_reached() __attribute__((noreturn))
Definition: util.cc:42
Definition: function.h:44
void run()
Definition: fiber.cc:552
const auto & stackBuffer() const
Definition: fiber.h:180
std::unique_ptr< detail::Fiber > main
Definition: fiber.h:48
auto type()
Definition: fiber.h:177
std::pair< char *, char * > activeRegion() const
Definition: fiber.cc:253
size_t allocatedSize() const
Definition: fiber.h:98
Definition: reference.h:47
std::unique_ptr< detail::Fiber > switch_trampoline
Definition: fiber.h:51
std::vector< std::unique_ptr< Fiber > > cache
Definition: fiber.h:60
Definition: fiber.h:295
resumable::Handle * handle()
Definition: fiber.h:329
Definition: location.h:94
bool hasResult() const
Definition: fiber.h:335
Definition: fiber.h:66
Definition: result.h:67
std::string fmt(const char *fmt, const Args &... args)
Definition: fmt.h:13
Resumable(Function f)
Definition: fiber.h:304
Definition: fiber.h:151