Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Event Loop API

yev_loop is the asynchronous event loop that drives every Yuneta yuno. It is built on Linux io_uring (not epoll) for zero-syscall-per-op submission/completion.

What it provides

A full submission queue

Every operation is submitted to the kernel as soon as it is prepared. When the submission queue has no free entry, the loop first flushes it (io_uring_submit()) and asks again. When the kernel still takes nothing (io_uring_enter() fails; on an older kernel a CQ overflow answers EBUSY until the completions are reaped), the submission is kept by the loop, and one WARNING says it: “Submission queue full and the kernel takes nothing: kept for the next cycle” (with ret and sret, the error of the flush). A submission made while others are kept is kept after them, so the kernel receives them in order (two writes of one socket).

A failed io_uring_submit() can also leave its entry in the queue when the queue has room. The loop does not wait for that entry to be submitted by chance: at the start of each cycle it looks for submissions that the kernel did not take, kept or in the queue, and submits them again. One WARNING says it when this starts: “Submissions the kernel did not take: submitted again at each cycle” (with kept and in_queue). While some are not taken, the loop waits at most 10 ms for a completion and tries again (this is not the timeout of the loop: its callback is not called). After 100 cycles an ERROR says that the operations still wait: “Submissions not taken by the kernel for many cycles of the loop: their operations wait” (with cycles, kept, in_queue, ret and sret). When the kernel takes them after that, an INFO says it: “Submissions taken by the kernel again”.

For the caller nothing changes: yev_start_event(), yev_start_timer_event(), yev_stop_event() and yev_loop_stop() answer 0, and the event is RUNNING (or CANCELING) as after any submission.

A stop of an event whose submission the kernel did not take yet (kept, or still in the queue) takes it back. The kernel never saw it, and if it is submitted later it runs on an fd that the stop closed (a timer, a connect), maybe already used by a new event. The loop finds the submission by its event, not by its fd number. An entry in the queue becomes a NOP whose completion is not delivered. The loop completes the event as a cancel does, and the callback gets the event STOPPED with result -ECANCELED at the next cycle. That completion needs a place in a list of the loop: when there is no memory for it, the take-back is not made, a CRITICAL says so (“No memory to keep a completion: submission handed over as it is”): the submission goes to the kernel as it is, and the stop cancels it there as it cancels any operation the kernel has.

The same applies to the submissions of other events on an fd that the loop closes. A connect, an accept and a timer event own their fd: the loop closes it at a stop (connect, timer) or at the free of the event. The writes of C_TCP are other events on the socket of its connect event. Before the close, every submission on that fd that the kernel did not take yet is taken back as above, and its event gets its callback STOPPED, -ECANCELED, at the next cycle, with a WARNING: “An fd is closed with submissions of other events on it that the kernel did not take: taken back, completed as canceled” (with fd and type). What the kernel already has is not touched: the kernel holds its own reference to the file. Without memory for that completion the submissions are dropped all the same, with an ERROR (“...: dropped, the event will not complete”): an event that waits is better than bytes sent to another file. Handed to the kernel later, they would run on whatever file had taken the number: the bytes of an old connection would go to a new peer, and the write callback would say they were sent. (7.25.4 kept no submission; an entry that a failed submit left in the queue went to the kernel at the next submit that worked, on whatever file had the number by then.)

The owner of a submission is read from the entry itself: its fd and its event (user_data). An entry of the ring that the loop has handed out but not yet prepared already sits between the head and the tail of the queue, and the kernel does not clear a slot it has read: it would still name the file and the event of the LAST operation of that slot. So every entry the loop hands out is cleared of both (fd -1, no event) before it is prepared, and the stop of a RUNNING timer or connect prepares its cancel before it closes the fd. A scan made in between would otherwise “take back” an old event that has nothing untaken -- an extra STOPPED callback, a wrong count of its completions, and, if that event was already freed, a completion on freed memory. Every yuno wraps its ring (2400 entries) and reuses fd numbers, so the case is ordinary, not rare.

/*
 *  A timer RUNNING in a ring that has wrapped: every slot once held the
 *  fd and the pointer of this timer
 */
yev_start_timer_event(timer, 10000, FALSE);
yev_stop_event(timer);          // 0: one cancel, nothing taken back
yev_loop_run(yev_loop, 1);      // callback: timer STOPPED, once
/*
 *  A write of a connection whose socket is closed before the kernel
 *  took the write: the write is canceled, it never reaches the file
 *  that takes the number next
 */
yev_event_h wr = yev_create_write_event(yev_loop, callback, gobj, yev_get_fd(yev_connect), gbuf);
yev_start_event(wr);            // 0, kept: the kernel takes nothing now
yev_stop_event(yev_connect);    // IDLE: its socket is closed, the write taken back
yev_loop_run(yev_loop, 1);      // callback: wr STOPPED, result -ECANCELED

In 7.25.4 and earlier, a full queue ended the process at each of the 12 places that ask for an entry: 10 used the NULL entry (a crash), and 2 logged “io_uring_get_sqe() FAILED” and aborted. An entry left in the queue by a failed submit waited for the next submit that worked, which could be never. The answer is -1 now only when there is no memory to keep the submission: a CRITICAL “No memory to keep a submission”, then an ERROR of the caller that says what did not happen (“No memory to keep a submission: event NOT started”, “...: timer NOT started”, “...: accept event NOT re-armed”, ...). No caller aborts the process.

/*
 *  A timer started while the kernel takes no submission: kept, and armed
 *  at the next cycle of the loop. yev_start_timer_event() answers 0.
 */
yev_event_h timer = yev_create_timer_event(yev_loop, callback, gobj);
yev_start_timer_event(timer, 100, FALSE);   // 0, the event is RUNNING
yev_loop_run(yev_loop, 1);                  // submitted here; the callback gets it IDLE

/*
 *  Stopped before the kernel took it: the callback gets it STOPPED,
 *  result -ECANCELED, and nothing runs on the closed fd
 */
yev_start_timer_event(timer, 10*1000, FALSE);
yev_stop_event(timer);                      // 0
yev_loop_run(yev_loop, 1);

The tests are tests/c/yev_loop/yev_events/test_yevent_sq_full.c (a full queue), test_yevent_sq_retry.c (a failed submit, a stop with the fd used again, many cycles), test_yevent_sq_nomem.c (no memory to keep) and test_yevent_close_fd_kept.c (the fd of a connect closed with a write of another event kept, and in the queue).

Zero-copy sends

A sendmsg event (yev_create_sendmsg_event(), used by C_UDP_S) is sent with io_uring_prep_sendmsg_zc(). The kernel may not copy the data: it reads the buffer of the event until the datagram is transmitted. So one send gives two completions:

CompletionresFlagMeaning
1stbytes sent, or -errnoIORING_CQE_F_MOREThe result of the send. A second completion follows.
2nd0IORING_CQE_F_NOTIFThe kernel does not use the buffer any more.

An error can come with IORING_CQE_F_MORE too (-EMSGSIZE, -EBADF). When the first completion has no IORING_CQE_F_MORE, no second completion comes.

The loop does this with them:

/*
 *  Send one datagram and destroy the event in its callback, as C_UDP_S
 *  does when all the data is sent. The callback is called once. The loop
 *  frees the event, and releases the gbuffer, after the notification.
 */
PRIVATE int send_callback(yev_event_h yev_event)
{
    if(yev_get_state(yev_event) == YEV_ST_IDLE) {
        // yev_get_result(yev_event) = bytes sent
    } else {
        // STOPPED: yev_get_result(yev_event) = -errno, for example -EMSGSIZE
    }
    yev_destroy_event(yev_event);   // safe: the free waits for the notification
    return 0;
}

gbuffer_t *gbuf = gbuffer_create(256, 256);
gbuffer_append_string(gbuf, "hello");
yev_event_h ev = yev_create_sendmsg_event(
    yev_loop, send_callback, gobj, fd, gbuf,
    (struct sockaddr *)&dst_addr, sizeof(dst_addr)
);
yev_start_event(ev);

In 7.25.4 and earlier the loop counted one completion for each submission, and called the callback for both completions. An event destroyed at the first completion was freed there, and the notification read the freed event (a use-after-free). A callback that did not destroy the event was called a second time with result 0.

The test is tests/c/yev_loop/yev_events/test_yevent_udp_zerocopy.c.

A stop keeps the gbuffer

A stop of a RUNNING read, write, recvmsg or sendmsg event submits a cancel. The cancel is not done when it is submitted: until the completion of the operation arrives, the kernel can still write into the gbuffer (a read) or read from it (a write). So the event keeps its gbuffer while it has a completion to come:

A reader that connects again, as C_TCP does:

PRIVATE int yev_callback(yev_event_h yev_event)
{
    if(yev_get_state(yev_event) == YEV_ST_STOPPED) {
        // Do not free yev_get_gbuf(yev_event): the loop releases it, at the
        // last completion of the event (usually before this callback)
        return 0;
    }
    ...
}

// Connected again: the same read event, with a new gbuffer
if(!yev_get_gbuf(yev_reading)) {
    yev_set_gbuffer(yev_reading, gbuffer_create(rx_buffer_size, rx_buffer_size));
} else {
    gbuffer_clear(yev_get_gbuf(yev_reading));
}
yev_start_event(yev_reading);

In 7.25.4 and earlier the stop released the gbuffer at once, for every type of event. Its memory could be freed and used again while the kernel still had the read or the write.

The test is tests/c/yev_loop/yev_events/test_yevent_stop_in_flight.c.

The end of a loop

An event destroyed with a completion still to come is freed by the loop at that completion (see yev_destroy_event()). The loop keeps a list of these events, so that none is lost when the loop ends first:

A yuno ends like this, and it needs nothing more:

yev_loop_run(yev_loop, -1);     // until the yuno must die
stop_services();                // the transports stop their events
gobj_end();                     // the gobjs destroy their events
yev_loop_stop(yev_loop);
yev_loop_destroy(yev_loop);     // frees the events still waiting

In 7.25.4 and earlier, an event destroyed while the loop ran, whose completion did not come before the loop ended (a callback broke the loop, a zero-copy notification was late), was never freed: a leak of the event and its gbuffer. And an event destroyed after the loop ended was freed at once, while the kernel could still have its read or its write.

The test is tests/c/yev_loop/yev_events/test_yevent_loop_end_drain.c.

IPv6 peers

A peer address is kept with its real length. An IPv4 address (struct sockaddr_in) has 16 bytes, an IPv6 address (struct sockaddr_in6) has 28 bytes:

An echo, as C_UDP_S does it. It works for an IPv4 or an IPv6 peer:

PRIVATE int recv_callback(yev_event_h yev_event)
{
    if(yev_get_state(yev_event) == YEV_ST_IDLE) {
        gbuffer_t *gbuf = yev_get_gbuf(yev_event);

        // The peer, with the length that the kernel gave
        gbuffer_setaddr(
            gbuf,
            yev_event->msghdr->msg_name,
            yev_event->msghdr->msg_namelen
        );

        // The address lives in the gbuffer, and the send event holds the gbuffer
        yev_event_h yev_reply = yev_create_sendmsg_event(
            yev_get_loop(yev_event), send_callback, gobj, yev_get_fd(yev_event),
            gbuffer_incref(gbuf),
            gbuffer_getaddr(gbuf),
            gbuffer_getaddrlen(gbuf)
        );
        yev_start_event(yev_reply);
    }
    return 0;
}

In 7.25.4 and earlier the address was a struct sockaddr (16 bytes) in sock_info_t and in the gbuffer, and a sendmsg event always gave the kernel 16 bytes. An accept or connect event refused an IPv6 address, a received IPv6 peer was cut to 16 bytes, and the kernel refused a reply to it (-EINVAL). So C_UDP_S could not answer an IPv6 peer.

The test is tests/c/yev_loop/yev_events/test_yevent_udp_ipv6.c.

Static-build helpers

yev_loop.c also exposes yuneta_getaddrinfo() / yuneta_freeaddrinfo() — a UDP DNS resolver that reads /etc/resolv.conf and /etc/hosts directly, bypassing glibc’s NSS layer. When CONFIG_FULLY_STATIC is enabled, all getaddrinfo / freeaddrinfo call sites are redirected to these via macros, since glibc’s resolver is not available in a fully static build.

Like glibc, it sends no numeric host to DNS. An address of the family asked for is answered directly. An address of the other family is answered at once, as glibc answers it: an IPv4 address in AF_INET6 with AI_V4MAPPED is its v4-mapped IPv6 address, a v4-mapped IPv6 address in AF_INET is its IPv4 address, and any other answers EAI_ADDRFAMILY. AI_NUMERICHOST with a name answers EAI_NONAME:

struct addrinfo hints = {.ai_family = AF_INET, .ai_socktype = SOCK_STREAM};
struct addrinfo *res;
int ret = getaddrinfo("::1", "0", &hints, &res);   // EAI_ADDRFAMILY, no DNS query

hints.ai_family = AF_INET6;
hints.ai_flags = AI_V4MAPPED;
ret = getaddrinfo("127.0.0.1", "0", &hints, &res); // 0: ::ffff:127.0.0.1

Up to 7.25.20 the two mapped cases answered EAI_ADDRFAMILY too.

A numeric IPv4 address is what glibc takes as one: every form of inet_aton() -- the shorthand, a single number, hex and octal parts -- with nothing before or after it:

getaddrinfo("127.1", "0", &hints, &res);        // 127.0.0.1, no DNS query
getaddrinfo("10.1.2", "0", &hints, &res);       // 10.1.0.2
getaddrinfo("0x7f.1", "0", &hints, &res);       // 127.0.0.1 (0177.0.0.1 too)
getaddrinfo("127.0.0.1 ", "0", &hints, &res);   // a name (a blank after it)

Up to 7.25.20 only the dotted quad was numeric: the other forms were sent to DNS, and AI_NUMERICHOST refused them.

Benchmarks & tests

Source code

Function reference

The individual function reference pages are listed in the left-hand sidebar under Event Loop API.

yev_create_accept_event()

yev_create_accept_event() creates a new accept event associated with the given event loop and callback function.

yev_event_h yev_create_accept_event(
    yev_loop_h yev_loop,
    yev_callback_t callback,
    const char *listen_url,
    int backlog,
    BOOL shared,
    int ai_family,
    int ai_flags,
    hgobj gobj
);

Parameters

KeyTypeDescription
yev_loopyev_loop_hThe event loop handle in which the accept event will be created.
callbackyev_callback_tThe callback function to be invoked when the event is triggered. If it returns -1, the loop in yev_loop_run() will break.
listen_urlconst char *The URL to listen on (for example "tcp://0.0.0.0:7000").
backlogintQueue size of pending connections for socket listening.
sharedBOOLWhether to open the socket as shared (SO_REUSEPORT).
ai_familyintAddress family (for example AF_UNSPEC, AF_INET, AF_INET6).
ai_flagsintAddress info flags (for example AI_V4MAPPED | AI_ADDRCONFIG).
gobjhgobjThe associated hgobj object for event handling.

Returns

Returns a yev_event_h handle to the newly created accept event, or NULL on failure.


yev_create_connect_event()

yev_create_connect_event() creates a new connect event associated with the specified event loop and callback function.

yev_event_h yev_create_connect_event(
    yev_loop_h yev_loop,
    yev_callback_t callback,
    const char *dst_url,
    const char *src_url,
    int ai_family,
    int ai_flags,
    hgobj gobj
);

Parameters

KeyTypeDescription
yev_loopyev_loop_hThe event loop handle in which the connect event will be created.
callbackyev_callback_tThe callback function to be invoked when the event is triggered. If it returns -1, the loop in yev_loop_run() will break.
dst_urlconst char *Destination URL to connect to (for example "tcp://host:port").
src_urlconst char *The local address to bind before the connect: "host:port", "[ipv6]:port" or "schema://host:port", or NULL. An empty host binds any address, port 0 any port. A bad src_url is an error: logged, and the event has no socket.
ai_familyintAddress family (for example AF_UNSPEC, AF_INET, AF_INET6).
ai_flagsintAddress info flags (for example AI_V4MAPPED | AI_ADDRCONFIG).
gobjhgobjThe associated GObj instance for event handling.

Returns

Returns a yev_event_h handle to the newly created connect event, or NULL on failure.

Notes

The host is resolved in the family of the destination address. In 7.25.4 a non-empty src_url was never parsed: a dynamic build failed the connect with “getaddrinfo() src_url FAILED”, and a static one bound the socket to the loopback address with a port of the kernel’s choice, whatever the src_url said.

A destination name can resolve to several addresses (localhost is ::1 and 127.0.0.1). An address in whose family the src_url has no address (a src 127.0.0.1 and the destination ::1) is skipped silently, and the next address is tried; only when no address is left is it an ERROR, “Cannot get addr to connect”, with the src_url.

// localhost is [::1] first here: the IPv6 address is skipped, the connect goes to 127.0.0.1
yev_event_h ev = yev_create_connect_event(
    yev_loop, callback, "tcp://localhost:5000", "127.0.0.1:40000", AF_UNSPEC, 0, gobj
);

A src_url that fails otherwise is an ERROR, and the event has no socket: “Bad src_url: cannot bind the connect” for a src_url that cannot be parsed ("[::1:5000", a missing ]) or does not fit, “getaddrinfo() src_url FAILED” for a host that cannot be resolved for another reason (the resolver fails), “bind() src_url FAILED” for a bind the kernel refuses (a port in use, an address that is not the node’s), with errno.

// Connect to [::1]:5000 from the local port 40000
yev_event_h ev = yev_create_connect_event(
    yev_loop, callback, "tcp://[::1]:5000", "[::1]:40000", AF_UNSPEC, 0, gobj
);
if(yev_get_fd(ev) < 0) {
    // bad url or src_url: already logged
}
yev_start_event(ev);

yev_create_read_event()

yev_create_read_event() creates a new read event associated with a given event loop, callback function, file descriptor, and buffer.

yev_event_h yev_create_read_event(
    yev_loop_h      yev_loop,
    yev_callback_t  callback,
    hgobj           gobj,
    int            fd,
    gbuffer_t *    gbuf
);

Parameters

KeyTypeDescription
yev_loopyev_loop_hThe event loop handle in which the read event will be registered.
callbackyev_callback_tThe function to be called when the read event is triggered. If it returns -1, the loop in yev_loop_run() will break.
gobjhgobjThe associated object that will handle the event.
fdintThe file descriptor to monitor for read events.
gbufgbuffer_t *The buffer where the read data will be stored.

Returns

Returns a yev_event_h handle to the newly created read event, or NULL if the creation fails.

Notes

The event will be monitored for readability, and when data is available, the specified callback function will be invoked.


yev_create_timer_event()

yev_create_timer_event() creates a new timer event associated with the specified event loop and callback function.

yev_event_h yev_create_timer_event(
    yev_loop_h      yev_loop,
    yev_callback_t  callback,
    hgobj           gobj
);

Parameters

KeyTypeDescription
yev_loopyev_loop_hThe event loop handle in which the timer event will be created.
callbackyev_callback_tThe callback function to be invoked when the timer event triggers. If it returns -1, the loop in yev_loop_run() will break.
gobjhgobjThe associated hgobj object for the event.

Returns

Returns a yev_event_h handle to the newly created timer event, or NULL on failure.

Notes

The timer event must be started using yev_start_timer_event() before it becomes active.


yev_create_write_event()

yev_create_write_event() creates a write event associated with a given file descriptor and buffer within the specified event loop.

yev_event_h yev_create_write_event(
    yev_loop_h      yev_loop,
    yev_callback_t  callback,
    hgobj           gobj,
    int            fd,
    gbuffer_t *    gbuf
);

Parameters

KeyTypeDescription
yev_loopyev_loop_hThe event loop in which the write event will be created.
callbackyev_callback_tThe callback function to be invoked when the event is triggered. If it returns -1, yev_loop_run() will break.
gobjhgobjThe associated GObj instance for event handling.
fdintThe file descriptor to be monitored for write readiness.
gbufgbuffer_t *The buffer containing data to be written. If NULL, no buffer is associated.

Returns

Returns a handle to the newly created write event (yev_event_h). If creation fails, NULL is returned.

Notes

The write event monitors the specified file descriptor for write readiness. Use yev_set_gbuffer() to modify the associated buffer.


yev_destroy_event()

yev_destroy_event() releases the resources associated with a given event. This makes sure of proper cleanup.

void yev_destroy_event(
    yev_event_h yev_event
);

Parameters

KeyTypeDescription
yev_eventyev_event_hHandle to the event that will be destroyed.

Returns

This function does not return a value.

Notes

The socket of a timer, accept or connect event (the fd the event created) is closed before destruction. A read, write, recvmsg, sendmsg or poll event does not own its fd: it is the caller’s, and it stays open. An event with a completion still to come (a running or canceled operation, or the notification of a zero-copy send) is not freed at once, also when the loop does not run: its callback is not called again, and the loop frees it at its last completion, or in yev_loop_destroy(). See The end of a loop.

yev_stop_event(yev_reading);    // a RUNNING read: the cancel is submitted
yev_destroy_event(yev_reading); // freed at the completion of the cancel

yev_event_type_name()

yev_event_type_name() returns a string representation of the event type associated with the given yev_event_h handle.

const char *yev_event_type_name(
    yev_event_h yev_event
);

Parameters

KeyTypeDescription
yev_eventyev_event_hHandle to the event whose type name is to be retrieved.

Returns

A pointer to a constant string representing the event type name.

Notes

The returned string is statically allocated and must not be modified or freed by the caller.


yev_flag_strings()

yev_flag_strings() returns an array of string representations for yev_flag_t enumeration values.

const char **yev_flag_strings(void);

Parameters

KeyTypeDescription
--This function does not take any parameters.

Returns

A pointer to a NULL-terminated array of strings representing yev_flag_t values.

Notes

The returned array provides human-readable names for yev_flag_t flags, which can be useful for debugging and logging.


yev_get_state_name()

yev_get_state_name() retrieves the name of the current state of the specified event.

const char *yev_get_state_name(
    yev_event_h yev_event
);

Parameters

KeyTypeDescription
yev_eventyev_event_hThe event handle whose state name is to be retrieved.

Returns

A string representing the name of the current state of the event.

Notes

The returned string corresponds to one of the predefined event states.


yev_get_waiting_completion()

yev_get_waiting_completion() says whether the kernel has posted a completion of yev_event that the loop has not delivered yet: it is in the completion ring, behind the one being delivered. The ring is looked at, not consumed. Between two turns of the loop the kernel completes an operation at any return to user space, so what a read took from its file may already be out of the file and not yet in the owner’s hands.

int yev_get_waiting_completion(
    yev_event_h yev_event,
    int         *result
);

Parameters

KeyTypeDescription
yev_eventyev_event_hThe event whose completion is looked for.
resultint *Filled with the result of the completion found (a read: the bytes it took). May be NULL.

Returns

1 when a completion waits (*result set), 0 when none does, -1 when it cannot be known: completions overflowed the ring and wait in the kernel.

Do not call it from the callback of yev_event itself: the completion being delivered is still in the ring then (the loop marks it seen after the callback), and it is found as a waiting one. It looks at the ring only, not at the completions the loop keeps aside to deliver later (kept_cqes).

Example

fs_watcher counts the events of an inotify fd that a read has taken and the loop has not handed over, besides those the kernel still holds. The ring is looked at before and after asking the kernel: the same answer both times says nothing completed in between.

int res1 = 0, res2 = 0, queued = 0;
int w1 = yev_get_waiting_completion(yev_event, &res1);
ioctl(fd, FIONREAD, &queued);
int w2 = yev_get_waiting_completion(yev_event, &res2);
if(w1 >= 0 && w1 == w2 && res1 == res2) {
    size_t not_delivered = (w2 && res2 > 0? res2 : 0) + queued;
}

yev_get_yuno()

yev_get_yuno() retrieves the yuno object associated with the given event loop.

hgobj yev_get_yuno(
    yev_loop_h yev_loop
);

Parameters

KeyTypeDescription
yev_loopyev_loop_hHandle to the event loop from which to retrieve the yuno object.

Returns

Returns the hgobj object associated with the specified event loop.

Notes

The returned hgobj can be NULL if the event loop is not properly initialized.


yev_loop_create()

yev_loop_create() initializes a new event loop associated with a given hgobj instance, allocating resources for event management.

int yev_loop_create(
    hgobj          yuno,
    unsigned       entries,
    int           keep_alive,
    yev_callback_t callback,
    yev_loop_h    *yev_loop
);

Parameters

KeyTypeDescription
yunohgobjThe hgobj instance associated with the event loop.
entriesunsignedThe maximum number of event entries the loop can handle.
keep_aliveintSpecifies whether the loop must persist after processing events.
callbackyev_callback_tA callback function invoked for each event. Returning -1 will break yev_loop_run().
yev_loopyev_loop_h *Pointer to store the created event loop handle.

Returns

Returns 0 on success, or a negative value on failure.

Notes

If callback is NULL, a default callback will be used when processing events in yev_loop_run().

It first checks that the kernel’s io_uring has every operation the loop uses (the probe of the opcodes) and the cancel of everything still in flight with which a loop stops (IORING_ASYNC_CANCEL_ALL | ANY, tried once): Linux 5.19 or later, or the 5.14 of RHEL/Rocky/Alma 9. Without them it logs a CRITICAL and aborts: “Linux kernel too old for yunetas: ...”, with lacks and kernel. See Linux kernel.


yev_loop_destroy()

yev_loop_destroy() releases all resources associated with the given event loop and terminates its execution.

void yev_loop_destroy(
    yev_loop_h yev_loop
);

Parameters

KeyTypeDescription
yev_loopyev_loop_hHandle to the event loop to be destroyed.

Returns

This function does not return a value.

Notes

After calling yev_loop_destroy(), the yev_loop_h handle becomes invalid and must not be used. Before it closes the ring, it frees the destroyed events whose completions have not come: it cancels what the kernel still has, reaps the completions for 1 second at most (no callback is called), and frees what is left then with an ERROR “Loop destroyed with events whose completions did not come: freed”. A zero-copy send whose notification has not come is waited for up to 5 seconds, and then NOT freed, with a WARNING “Loop destroyed with zero-copy sends whose notification did not come: NOT freed, the kernel may still read their gbuffer”: the kernel may still read its gbuffer. When there is no submission entry for that cancel, it logs “Submission queue full: the cancel of the events left is NOT submitted, their completions may not come” first. See The end of a loop.

yev_loop_stop(yev_loop);
yev_loop_run_once(yev_loop);    // optional: reaps what is ready
yev_loop_destroy(yev_loop);     // frees the destroyed events still waiting

yev_loop_reset_running()

yev_loop_reset_running() resets the running state of the given event loop, clearing any active execution flags.

void yev_loop_reset_running(
    yev_loop_h yev_loop
);

Parameters

KeyTypeDescription
yev_loopyev_loop_hHandle to the event loop whose running state will be reset.

Returns

This function does not return a value.

Notes

Use yev_loop_reset_running() to make sure that the event loop is reset before restarting it.


yev_loop_run()

yev_loop_run() starts the event loop and processes events until stopped or a timeout occurs.

int yev_loop_run(
    yev_loop_h yev_loop,
    int        timeout_in_seconds
);

Parameters

KeyTypeDescription
yev_loopyev_loop_hHandle to the event loop instance.
timeout_in_secondsintMaximum time in seconds to run the loop before returning.

Returns

Returns 0 on successful execution, or -1 if an error occurs.

Notes

If a callback function returns -1, the loop will break and exit early.

Every cycle begins with gobj_deliver_posted_events(), which delivers what the gobjs posted with gobj_post_event(). It happens before the completions, so an event posted while the loop was not yet running does not wait for a completion that may never arrive. While messages are pending, or the loop holds a completion it made itself (a stop that took back a submission the kernel had not taken, see yev_stop_event()), the loop does not block on the ring: it takes a completion if one is ready, and returns to the queue if not.


yev_loop_run_once()

yev_loop_run_once() executes a single iteration of the event loop, processing one event if available.

int yev_loop_run_once(
    yev_loop_h yev_loop
);

Parameters

KeyTypeDescription
yev_loopyev_loop_hHandle to the event loop instance.

Returns

Returns 0 on success, or a negative value on failure.

Notes

This function processes at most one event and then returns immediately. To continuously process events, use yev_loop_run().

One turn also means one delivery of the posted events: it calls gobj_deliver_posted_events() before it reads the completions. The callers of this function use it to let pending work settle, and a posted event is pending work.


yev_loop_stop()

yev_loop_stop() stops the execution of the event loop, transitioning it to the idle state.

int yev_loop_stop(
    yev_loop_h yev_loop
);

Parameters

KeyTypeDescription
yev_loopyev_loop_hHandle to the event loop to be stopped.

Returns

Returns 0 on success, or a negative value on failure.

Notes

Stopping the event loop using yev_loop_stop() will cause it to exit its execution cycle, but it can be restarted using yev_loop_run(). With a full submission queue that the kernel does not take, the stop is kept and made at the next cycle of the loop (see A full submission queue).


yev_protocol_set_protocol_fill_hints_fn()

yev_protocol_set_protocol_fill_hints_fn() sets a custom function to fill protocol hints based on a given schema.

int yev_protocol_set_protocol_fill_hints_fn(
    yev_protocol_fill_hints_fn_t yev_protocol_fill_hints_fn
);

Parameters

KeyTypeDescription
yev_protocol_fill_hints_fnyev_protocol_fill_hints_fn_tA function pointer that defines how protocol hints must be filled based on the schema.

Returns

Returns 0 on success, or -1 on failure.

Notes

This function allows customization of protocol hint filling, which is useful for adapting to different network configurations.


yev_set_gbuffer()

yev_set_gbuffer() associates a gbuffer_t * with a given yev_event_h. If a previous buffer exists, it is freed before setting the new one.

int yev_set_gbuffer(
    yev_event_h  yev_event,
    gbuffer_t   *gbuf
);

Parameters

KeyTypeDescription
yev_eventyev_event_hThe event handle to which the buffer will be assigned.
gbufgbuffer_t *The buffer to associate with the event. If NULL, the current buffer is reset.

Returns

Returns 0 on success, or -1 if an error occurs.

Notes

This function is only applicable for events created using yev_create_read_event() and yev_create_write_event(). When the event has an operation in the kernel, its gbuffer is not released at once: NULL releases it at the last completion, and a new gbuffer is refused with an error (the new gbuffer is released). See A stop keeps the gbuffer.

if(!yev_get_gbuf(yev_event)) {
    yev_set_gbuffer(yev_event, gbuffer_create(4096, 4096));
}

yev_start_event()

yev_start_event() starts the specified event, transitioning it to the running state if applicable.

int yev_start_event(
    yev_event_h yev_event
);

Parameters

KeyTypeDescription
yev_eventyev_event_hHandle to the event that must be started.

Returns

Returns 0 on success, or a negative value on failure.

Notes

For timer events, use yev_start_timer_event() instead. With a full submission queue that the kernel does not take, the submission is kept and made at the next cycle of the loop, and the answer is 0 (see A full submission queue).


yev_start_timer_event()

yev_start_timer_event() starts a timer event, creating the handler file descriptor if it does not exist.

int yev_start_timer_event(
    yev_event_h yev_event,
    time_t      timeout_ms,
    BOOL        periodic
);

Parameters

KeyTypeDescription
yev_eventyev_event_hHandle to the event that will be started as a timer.
timeout_mstime_tTimeout in milliseconds. A value of timeout_ms <= 0 is equivalent to calling yev_stop_event().
periodicBOOLIf TRUE, the timer will be periodic. Otherwise, it will be a one-shot timer.

Returns

Returns 0 on success, or -1 on failure.

Notes

To start a timer event, use yev_start_timer_event() instead of yev_start_event(). If the timer is in the IDLE state, it can be reused. If it is STOPPED, a new timer event must be created.


yev_stop_event()

yev_stop_event() stops the specified event. This makes sure that its associated file descriptor is closed if applicable. This operation is idempotent. This means it can be called multiple times without adverse effects.

int yev_stop_event(
    yev_event_h yev_event
);

Parameters

KeyTypeDescription
yev_eventyev_event_hHandle to the event that must be stopped.

Returns

Returns 0 on success, or -1 if an error occurs.

Notes

If the event is a connect or a timer event, its fd is closed. The fd of an accept event (the listening socket) is never closed by a stop: it belongs to its owner. If the event is in an idle state, it can be reused. Otherwise, a new event must be created. A RUNNING event whose submission the kernel did not take yet, kept by the loop or still in the submission queue (see A full submission queue), is not canceled in the kernel: the submission is taken back, so it never runs on the closed fd, and the callback gets the event STOPPED with result -ECANCELED at the next cycle, as after a cancel. The gbuffer of an event with a completion to come is released at its LAST completion, not by the stop. See A stop keeps the gbuffer. A take-back made by a posted action (gobj_post_event()) is delivered at the next cycle too: the loop does not block while it holds a completion of its own.

-1 without memory for the cancel (the submission queue full, the kernel taking nothing, and no memory to keep the submission): “No memory to keep a submission: event NOT canceled”. The event is left exactly as it was -- RUNNING, with its gbuffer and its fd -- and its operation completes normally later, with its callback. In 7.25.4 a stop on a full submission queue released the gbuffer and closed the fd of a timer or a connect first, and then used a NULL entry: the process crashed.

A stop does not reach the callback of an event that is IDLE and not a timer (it becomes STOPPED at once), of an IDLE zero-copy send whose notification is still to come, and of a RUNNING event stopped by yev_destroy_event() while the loop is stopping (its callback is dropped). An IDLE timer gets its callback at once, STOPPED with -ECANCELED.

yev_stop_event(yev_reading);    // the read is canceled; its gbuffer waits for the completion
// ... the callback gets yev_reading STOPPED, -ECANCELED; the gbuffer is released
//     at the last completion (usually before the callback: yev_get_gbuf() == NULL)

yev_create_poll_event()

Creates a poll event for monitoring a file descriptor.

yev_event_h yev_create_poll_event(
    yev_loop_h yev_loop,
    yev_callback_t callback,
    hgobj gobj,
    int fd,
    unsigned poll_mask
);

Parameters

KeyTypeDescription
yev_loopyev_loop_hThe event loop handle in which the poll event will be created.
callbackyev_callback_tThe callback function to be invoked when the event is triggered. If it returns -1, the loop in yev_loop_run() will break.
gobjhgobjThe associated GObj instance for event handling.
fdintThe file descriptor to monitor.
poll_maskunsignedBitmask specifying the poll conditions to monitor (for example POLLIN, POLLOUT).

Returns

Returns a yev_event_h handle to the newly created poll event, or NULL on failure.


yev_create_recvmsg_event()

Creates a recvmsg event for receiving messages with socket address information.

yev_event_h yev_create_recvmsg_event(
    yev_loop_h yev_loop,
    yev_callback_t callback,
    hgobj gobj,
    int fd,
    gbuffer_t *gbuf
);

Parameters

KeyTypeDescription
yev_loopyev_loop_hThe event loop handle in which the recvmsg event will be created.
callbackyev_callback_tThe callback function to be invoked when a message is received. If it returns -1, the loop in yev_loop_run() will break.
gobjhgobjThe associated GObj instance for event handling.
fdintThe socket file descriptor to receive messages on.
gbufgbuffer_t *The buffer where the received data will be stored.

Returns

Returns a yev_event_h handle to the newly created recvmsg event, or NULL on failure.

Notes

After a receive, the peer address is in msghdr->msg_name (the addr of yev_get_sock_info(), a struct sockaddr_storage) and its length in msghdr->msg_namelen: 16 bytes for an IPv4 peer, 28 bytes for an IPv6 peer. See IPv6 peers.


yev_create_sendmsg_event()

Creates a sendmsg event for sending messages with a destination address.

yev_event_h yev_create_sendmsg_event(
    yev_loop_h yev_loop,
    yev_callback_t callback,
    hgobj gobj,
    int fd,
    gbuffer_t *gbuf,
    const struct sockaddr *dst_addr,
    socklen_t dst_addrlen
);

Parameters

KeyTypeDescription
yev_loopyev_loop_hThe event loop handle in which the sendmsg event will be created.
callbackyev_callback_tThe callback function to be invoked when the message was sent. If it returns -1, the loop in yev_loop_run() will break.
gobjhgobjThe associated GObj instance for event handling.
fdintThe socket file descriptor to send messages on.
gbufgbuffer_t *The buffer containing the data to be sent.
dst_addrconst struct sockaddr *Pointer to the destination socket address. It is not copied: it must live as long as the event (C_UDP_S gives the address kept in the gbuffer of the event).
dst_addrlensocklen_tThe length of dst_addr: sizeof(struct sockaddr_in) for IPv4, sizeof(struct sockaddr_in6) for IPv6.

Returns

Returns a yev_event_h handle to the newly created sendmsg event, or NULL on failure.

Notes

The send is zero-copy when the kernel has it: the callback is called once, and the loop frees a destroyed event only after the kernel releases the buffer. See Zero-copy sends.

BREAKING in 7.25.5: the dst_addrlen parameter is new. Up to 7.25.4 the event gave the kernel sizeof(struct sockaddr) (16 bytes), and a send to an IPv6 address failed with -EINVAL. See IPv6 peers. A start with no address, or with a length of 0 or larger than a struct sockaddr_storage, is refused: yev_start_event() answers -1 and logs “Cannot start event: sendmsg addr NULL or bad addr length” (7.25.4: “Cannot start event: sendmsg addr NULL”). C_UDP_S then drops that datagram (“Cannot send datagram: dropped”) and sends the next one.

struct sockaddr_in6 dst = {0};
dst.sin6_family = AF_INET6;
dst.sin6_addr = in6addr_loopback;
dst.sin6_port = htons(5000);

yev_event_h ev = yev_create_sendmsg_event(
    yev_loop, send_callback, gobj, fd, gbuf,
    (struct sockaddr *)&dst, sizeof(dst)
);
yev_start_event(ev);

yev_dup2_accept_event()

Creates a duplicate accept event from a raw listen socket file descriptor.

yev_event_h yev_dup2_accept_event(
    yev_loop_h yev_loop,
    yev_callback_t callback,
    int fd_listen,
    hgobj gobj
);

Parameters

KeyTypeDescription
yev_loopyev_loop_hThe event loop handle in which the accept event will be created.
callbackyev_callback_tThe callback function to be invoked when a connection is accepted. If it returns -1, the loop in yev_loop_run() will break.
fd_listenintThe raw listen socket file descriptor.
gobjhgobjThe associated GObj instance for event handling.

Returns

Returns a yev_event_h handle to the newly created accept event, or NULL on failure.


yev_dup_accept_event()

Creates a duplicate accept event based on an existing server accept event.

yev_event_h yev_dup_accept_event(
    yev_event_h yev_server_accept,
    int dup_idx,
    hgobj gobj
);

Parameters

KeyTypeDescription
yev_server_acceptyev_event_hHandle to the existing server accept event to duplicate.
dup_idxintIndex identifying the duplicate accept event.
gobjhgobjThe associated GObj instance for event handling.

Returns

Returns a yev_event_h handle to the newly created duplicate accept event, or NULL on failure.


yev_rearm_connect_event()

Prepares or reuses a connect event by establishing a connection to a destination URL.

int yev_rearm_connect_event(
    yev_event_h yev_event,
    const char *dst_url,
    const char *src_url,
    int ai_family,
    int ai_flags
);

Parameters

KeyTypeDescription
yev_eventyev_event_hHandle to the connect event to rearm.
dst_urlconst char *Destination URL to connect to (for example "tcp://host:port").
src_urlconst char *The local address to bind before the connect: "host:port", "[ipv6]:port" or "schema://host:port", or NULL. An empty host binds any address, port 0 any port. A bad src_url is an error: logged, and the event has no socket.
ai_familyintAddress family (for example AF_UNSPEC, AF_INET, AF_INET6).
ai_flagsintAddress info flags (for example AI_V4MAPPED | AI_ADDRCONFIG).

Returns

Returns the file descriptor on success, or -1 on error. The addresses of the destination are tried in order, as in yev_create_connect_event(): one in whose family the src_url has no address is skipped.


set_measure_times()

set_measure_times() enables per-operation latency measurement inside the event loop for a subset of yev_event types. The measurements are surfaced by get_measure_times() and are used by the ping-pong benchmarks under performance/c/.

void set_measure_times(int types); // -1 = all types

Parameters

KeyTypeDescription
typesintBitmask of yev_type_t values to measure, or -1 to measure every event type. Pass 0 to stop measuring.

Returns

This function does not return a value.

Notes

Measurement is off by default to avoid paying the cost on hot paths. Only turn it on for benchmarks or targeted diagnostics.


get_measure_times()

get_measure_times() returns the bitmask of yev_event types that currently have latency measurement enabled. Used together with set_measure_times().

int get_measure_times(void);

Returns

The bitmask of yev_type_t values being measured, or 0 if measurement is disabled.