// SPDX-License-Identifier: ISC
/*
 * Copyright (c) 2017-19  David Lamparter, for NetDEF, Inc.
 */

#ifndef _FRRCU_H
#define _FRRCU_H

#include <assert.h>

#include "memory.h"
#include "atomlist.h"

#ifdef __cplusplus
extern "C" {
#endif

/* quick RCU primer:
 *   There's a global sequence counter.  Whenever a thread does a
 *   rcu_read_lock(), it is marked as holding the current sequence counter.
 *   When something is cleaned with RCU, the global sequence counter is
 *   increased and the item is queued for cleanup - *after* all threads are
 *   at a more recent sequence counter (or no sequence counter / unheld).
 *
 *   So, by delaying resource cleanup, RCU ensures that things don't go away
 *   while another thread may hold a (stale) reference.
 *
 *   Note that even if a thread is in rcu_read_lock(), it is invalid for that
 *   thread to access bits after rcu_free() & co on them.  This is a design
 *   choice to allow no-op'ing out the entire RCU mechanism if we're running
 *   singlethreaded.  (Also allows some optimization on the counter bumping.)
 *
 * differences from Linux Kernel RCU:
 *   - there's no rcu_synchronize(), if you really need to defer something
 *     use rcu_call() (and double check it's really necessary)
 *   - rcu_dereference() and rcu_assign_pointer() don't exist, use atomic_*
 *     instead (ATOM* list structures do the right thing)
 */

/* opaque */
struct rcu_thread;

/* called before new thread creation, sets up rcu thread info for new thread
 * before it actually exits.  This ensures possible RCU references are held
 * for thread startup.
 *
 * return value must be passed into the new thread's call to rcu_thread_start()
 */
extern struct rcu_thread *rcu_thread_prepare(void);

/* cleanup in case pthread_create() fails */
extern void rcu_thread_unprepare(struct rcu_thread *rcu_thread);

/* called early in the new thread, with the return value from the above.
 * NB: new thread is initially in RCU-held state! (at depth 1)
 *
 * TBD: maybe inherit RCU state from rcu_thread_prepare()?
 */
extern void rcu_thread_start(struct rcu_thread *rcu_thread);

/* thread exit is handled through pthread_key_create's destructor function */

/* global RCU shutdown - must be called with only 1 active thread left.  waits
 * until remaining RCU actions are done & RCU thread has exited.
 *
 * This is mostly here to get a clean exit without memleaks.
 */
extern void rcu_shutdown(void);

/* enter / exit RCU-held state.  counter-based, so can be called nested. */
extern void rcu_read_lock(void);
extern void rcu_read_unlock(void);

/* for debugging / safety checks */
extern void rcu_assert_read_locked(void);
extern void rcu_assert_read_unlocked(void);

enum rcu_action_type {
	RCUA_INVALID = 0,
	/* used internally by the RCU code, shouldn't ever show up outside */
	RCUA_NEXT,
	RCUA_END,
	/* normal RCU actions, for outside use */
	RCUA_FREE,
	RCUA_CLOSE,
	RCUA_CALL,
};

/* since rcu_head is intended to be embedded into structs which may exist
 * with lots of copies, rcu_head is shrunk down to its absolute minimum -
 * the atomlist pointer + a pointer to this action struct.
 */
struct rcu_action {
	enum rcu_action_type type;

	union {
		struct {
			struct memtype *mt;
			ptrdiff_t offset;
		} free;

		struct {
			void (*fptr)(void *arg);
			ptrdiff_t offset;
		} call;
	} u;
};

/* RCU cleanup function queue item */
PREDECL_ATOMLIST(rcu_heads);
struct rcu_head {
	struct rcu_heads_item head;
	const struct rcu_action *action;
};

/* special RCU head for delayed fd-close */
struct rcu_head_close {
	struct rcu_head rcu_head;
	int fd;
};

/* enqueue RCU action - use the macros below to get the rcu_action set up */
extern void rcu_enqueue(struct rcu_head *head, const struct rcu_action *action);

/* RCU free() and file close() operations.
 *
 * freed memory / closed fds become _immediately_ unavailable to the calling
 * thread, but will remain available for other threads until they have passed
 * into RCU-released state.
 */

/* may be called with NULL mt to do non-MTYPE free() */
#define rcu_free(mtype, ptr, field)                                            \
	do {                                                                   \
		typeof(ptr) _ptr = (ptr);                                      \
		if (!_ptr)                                                     \
			break;                                                 \
		struct rcu_head *_rcu_head = &_ptr->field;                     \
		static const struct rcu_action _rcu_action = {                 \
			.type = RCUA_FREE,                                     \
			.u.free = {                                            \
				.mt = mtype,                                   \
				.offset = offsetof(typeof(*_ptr), field),      \
			},                                                     \
		};                                                             \
		rcu_enqueue(_rcu_head, &_rcu_action);                          \
	} while (0)

/* use this sparingly, it runs on (and blocks) the RCU thread */
#define rcu_call(func, ptr, field)                                             \
	do {                                                                   \
		typeof(ptr) _ptr = (ptr);                                      \
		void (*fptype)(typeof(ptr));                                   \
		struct rcu_head *_rcu_head = &_ptr->field;                     \
		static const struct rcu_action _rcu_action = {                 \
			.type = RCUA_CALL,                                     \
			.u.call = {                                            \
				.fptr = (void *)func,                          \
				.offset = offsetof(typeof(*_ptr), field),      \
			},                                                     \
		};                                                             \
		(void)(_fptype = func);                                        \
		rcu_enqueue(_rcu_head, &_rcu_action);                          \
	} while (0)

extern void rcu_close(struct rcu_head_close *head, int fd);

#ifdef __cplusplus
}
#endif

#endif /* _FRRCU_H */
