main-func: add DEFINE_MAIN_FUNCTION_FIBER (#42238)

Provides a Tokio-like #[tokio::main] entry point for binaries that want
to run their main function inside a fiber on a default sd_event loop.

DEFINE_MAIN_FUNCTION_FIBER(impl) mirrors DEFINE_MAIN_FUNCTION but routes
impl through a new run_main_fiber() helper, which:

  - allocates the default sd_event,
  - enables exit-on-idle so the loop terminates once all event sources
    drain (structured concurrency: impl is expected to cancel and reap
    whatever it spawned before returning),
  - spawns impl as a fiber named after program_invocation_short_name,
  - installs SIGINT/SIGTERM handlers that cancel the main fiber (and
    disable themselves so the loop can go idle once the fiber unwinds),
  - runs sd_event_loop and propagates impl's return value via
    sd_future_result, normalising -ECANCELED to 0 since that just
    represents an orderly signal-driven shutdown.
This commit is contained in:
Yu Watanabe
2026-08-17 04:34:22 +09:00
committed by GitHub
5 changed files with 248 additions and 0 deletions
+72
View File
@@ -3,9 +3,12 @@
#include <stdlib.h>
#include "sd-daemon.h"
#include "sd-event.h"
#include "sd-future.h"
#include "argv-util.h"
#include "ask-password-agent.h"
#include "log.h"
#include "main-func.h"
#include "pager.h"
#include "polkit-agent.h"
@@ -34,3 +37,72 @@ int exit_failure_if_negative(int result) {
int exit_failure_if_nonzero(int result) {
return result < 0 ? EXIT_FAILURE : result;
}
typedef struct MainFiberContext {
int argc;
char **argv;
main_fiber_func_t func;
int result;
bool exit_requested;
} MainFiberContext;
static int main_fiber_trampoline(void *userdata) {
MainFiberContext *ctx = ASSERT_PTR(userdata);
sd_event *event = ASSERT_PTR(sd_fiber_get_event());
ctx->result = ctx->func(ctx->argc, ctx->argv);
/* impl's result is an errno, the exit code an exit status that impl's exit handlers see: keep
* them apart. */
ctx->exit_requested = sd_event_get_exit_code(event, /* ret= */ NULL) >= 0;
if (!ctx->exit_requested)
(void) sd_event_exit(event, 0);
return ctx->result;
}
int run_main_fiber(int argc, char *argv[], main_fiber_func_t func) {
_cleanup_(sd_event_unrefp) sd_event *event = NULL;
MainFiberContext ctx = {
.argc = argc,
.argv = argv,
.func = func,
};
int r;
assert(func);
r = sd_event_default(&event);
if (r < 0)
return log_error_errno(r, "Failed to allocate event loop: %m");
/* The fiber gets a pointer into this frame and is only unwound by the loop we run below, which
* requires that we own that loop and that sd_fiber_new() arms the fiber's exit event source. */
if (sd_fiber_is_running() || sd_event_get_state(event) != SD_EVENT_INITIAL)
return log_error_errno(SYNTHETIC_ERRNO(EBUSY),
"Refusing to run a main fiber on a busy event loop.");
/* Fire-and-forget: the loop unwinds the fiber through its exit event source, and a suspended
* fiber is one we couldn't drop a reference to anyway. */
r = sd_fiber_new(event, program_invocation_short_name, main_fiber_trampoline, &ctx,
/* destroy= */ NULL, /* ret= */ NULL);
if (r < 0)
return log_error_errno(r, "Failed to spawn main fiber: %m");
r = sd_event_loop(event);
if (sd_event_get_state(event) != SD_EVENT_FINISHED)
/* Exit sources never ran, so the fiber may still be suspended with no working loop left
* to unwind it. Leak both.
*
* TODO: detach the loop from the default slot, it stays reachable with the abandoned
* fiber's sources armed on it, pointing into this frame. */
return log_error_errno(r, "Event loop failed: %m");
/* An impl cancelled by the exit request just echoes -ECANCELED back at us, so let the requested
* status speak. Anything else is impl's own verdict, and its successes stay out of the status. */
if (ctx.exit_requested && (ctx.result >= 0 || ctx.result == -ECANCELED))
return r;
return ctx.result < 0 ? ctx.result : 0;
}
+10
View File
@@ -34,3 +34,13 @@ int exit_failure_if_nonzero(int result) _const_;
* Note: "true" means failure! */
#define DEFINE_MAIN_FUNCTION_WITH_POSITIVE_FAILURE(impl) \
_DEFINE_MAIN_FUNCTION(, impl(argc, argv), exit_failure_if_nonzero)
typedef int (*main_fiber_func_t)(int argc, char *argv[]);
/* Spawn impl as a fiber on the default event loop and run the loop until it returns. impl follows the
* DEFINE_MAIN_FUNCTION convention: negative is failure, anything else success. sd_event_exit() picks an
* explicit exit status, cancels impl, and wins over its result. Signal policy belongs to impl. */
int run_main_fiber(int argc, char *argv[], main_fiber_func_t func);
#define DEFINE_MAIN_FUNCTION_FIBER(impl) \
_DEFINE_MAIN_FUNCTION(, run_main_fiber(argc, argv, impl), exit_failure_if_nonzero)
+2
View File
@@ -147,6 +147,8 @@ simple_tests += files(
'test-login-util.c',
'test-machine-util.c',
'test-macro.c',
'test-main-func-fiber.c',
'test-main-func.c',
'test-memfd-util.c',
'test-memory-util.c',
'test-mempool.c',
+16
View File
@@ -0,0 +1,16 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "main-func.h"
#include "tests.h"
static int run(int argc, char *argv[]) {
ASSERT_GE(argc, 1);
ASSERT_NOT_NULL(argv);
ASSERT_NOT_NULL(argv[0]);
ASSERT_NULL(argv[argc]);
/* A positive result means success, so this has to exit zero. */
return 23;
}
DEFINE_MAIN_FUNCTION_FIBER(run);
+148
View File
@@ -0,0 +1,148 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <signal.h>
#include "sd-event.h"
#include "sd-future.h"
#include "main-func.h"
#include "tests.h"
static char *test_argv[] = {
(char*) "alpha",
(char*) "beta",
NULL,
};
static void assert_test_argv(int argc, char *argv[]) {
ASSERT_EQ(argc, 2);
ASSERT_PTR_EQ(argv, test_argv);
ASSERT_STREQ(argv[0], "alpha");
ASSERT_STREQ(argv[1], "beta");
ASSERT_NULL(argv[2]);
}
static int run_main_fiber_checked(main_fiber_func_t func) {
_cleanup_(sd_event_unrefp) sd_event *event = NULL;
int r;
r = run_main_fiber(2, test_argv, func);
/* The main fiber, and with it the event loop it pins, has to be gone by the time run_main_fiber()
* returns, so allocating the default event again must hand out a fresh one. */
ASSERT_OK_POSITIVE(sd_event_default(&event));
return r;
}
static int return_argc(int argc, char *argv[]) {
assert_test_argv(argc, argv);
return argc;
}
static int return_cancelled(int argc, char *argv[]) {
assert_test_argv(argc, argv);
return -ECANCELED;
}
TEST(result) {
/* Positive results mean success and stay out of the exit status. */
ASSERT_OK_ZERO(run_main_fiber_checked(return_argc));
ASSERT_ERROR(run_main_fiber_checked(return_cancelled), ECANCELED);
}
static int nested_run(int argc, char *argv[]) {
assert_test_argv(argc, argv);
/* Attaching another main fiber to the running loop would leave it behind once we return. */
ASSERT_ERROR(run_main_fiber(argc, argv, return_argc), EBUSY);
return 0;
}
TEST(nested) {
ASSERT_OK_ZERO(run_main_fiber_checked(nested_run));
}
static int signal_to_raise;
static int raise_signal(int argc, char *argv[]) {
assert_test_argv(argc, argv);
ASSERT_OK(sd_event_set_signal_exit(sd_fiber_get_event(), true));
ASSERT_OK_ERRNO(raise(signal_to_raise));
ASSERT_ERROR(sd_fiber_suspend(), ECANCELED);
return -ECANCELED;
}
TEST(signal) {
FOREACH_ARGUMENT(signal_to_raise, SIGINT, SIGTERM)
ASSERT_OK_ZERO(run_main_fiber_checked(raise_signal));
}
static int fail_event(sd_event_source *s, void *userdata) {
ASSERT_NOT_NULL(s);
ASSERT_NULL(userdata);
return -EINVAL;
}
static bool fiber_unwound;
static void mark_fiber_unwound(bool *marker) {
ASSERT_NOT_NULL(marker);
fiber_unwound = true;
}
static int event_loop_error(int argc, char *argv[]) {
_cleanup_(sd_event_source_unrefp) sd_event_source *defer = NULL;
_unused_ _cleanup_(mark_fiber_unwound) bool unwind_marker = false;
assert_test_argv(argc, argv);
ASSERT_OK(sd_event_add_defer(sd_fiber_get_event(), &defer, fail_event, /* userdata= */ NULL));
ASSERT_OK(sd_event_source_set_exit_on_failure(defer, true));
ASSERT_ERROR(sd_fiber_suspend(), ECANCELED);
return -ECANCELED;
}
TEST(event_loop_error) {
fiber_unwound = false;
ASSERT_ERROR(run_main_fiber_checked(event_loop_error), EINVAL);
ASSERT_TRUE(fiber_unwound);
}
static int exit_event(int argc, char *argv[]) {
assert_test_argv(argc, argv);
ASSERT_OK(sd_event_exit(sd_fiber_get_event(), 23));
ASSERT_ERROR(sd_fiber_suspend(), ECANCELED);
return -ECANCELED;
}
static int exit_event_and_return(int argc, char *argv[]) {
assert_test_argv(argc, argv);
ASSERT_OK(sd_event_exit(sd_fiber_get_event(), 23));
return 0;
}
static int exit_event_and_fail(int argc, char *argv[]) {
assert_test_argv(argc, argv);
ASSERT_OK(sd_event_exit(sd_fiber_get_event(), 0));
return -ENOANO;
}
TEST(event_exit_code) {
ASSERT_EQ(run_main_fiber_checked(exit_event), 23);
ASSERT_EQ(run_main_fiber_checked(exit_event_and_return), 23);
/* Requesting an exit doesn't cancel a running impl, so its failure still wins. */
ASSERT_ERROR(run_main_fiber_checked(exit_event_and_fail), ENOANO);
}
DEFINE_TEST_MAIN(LOG_DEBUG);