Handles are the better pointers. More...
Data Structures | |
struct | bio_handle_t |
Macros | |
#define | BIO_INVALID_HANDLE ((bio_handle_t){ .index = 0 }) |
An invalid handle. | |
Functions | |
bio_handle_t | bio_make_handle (void *obj, const bio_tag_t *tag) |
Create a new handle. | |
void * | bio_resolve_handle (bio_handle_t handle, const bio_tag_t *tag) |
Resolve a handle, returning the associated pointer. | |
void * | bio_close_handle (bio_handle_t handle, const bio_tag_t *tag) |
Close a handle and invalidate it. | |
const bio_tag_t * | bio_handle_info (bio_handle_t handle) |
Retrieve the associated tag of a handle. | |
static int | bio_handle_compare (bio_handle_t lhs, bio_handle_t rhs) |
Compare two handles. | |
Handles are the better pointers.
The mechanism to manage handles are exposed so that user programs can also make use of it.
bio uses handles for all of its resources. This alleviates the problems of stale references and double free. In a concurrent environment, the lifetime of resources are often tricky to determine. Handles are safe to pass around and act on. Any operations on a stale handle will be noop.
Handle enables the following pattern:
In the above example, as soon as either the reader or the writer coroutine encounters an I/O error, it will break out of the loop and close the socket. This in turn cause the other coroutine to also break out. bio_net_close will be called twice on the same socket but it is safe and the associated resources will only be released once.
By making double free safe to execute, user code can be greatly simplified since there is no need to track when a resource was freed. In fact, freeing a resource that another coroutine is waiting on is the idiomatic way to signal to that coroutine to terminate. For example, in a web server, a route handler can close the listening socket to tell the acceptor coroutine to stop accepting new connections. This can be used to implement a "remote shutdown".
#define BIO_INVALID_HANDLE ((bio_handle_t){ .index = 0 }) |
An invalid handle.
Resolving this will always return NULL
. This macro is just a short hand for setting all fields in a bio_handle_t to 0. Thus, a handle can be zero-initialized, allowing "Zero Is Initialization" (ZII).
void * bio_close_handle | ( | bio_handle_t | handle, |
const bio_tag_t * | tag | ||
) |
Close a handle and invalidate it.
This is similar to bio_resolve_handle but it also invalidates the handle.
Take note that the associated pointer is also returned so that the calling code can perform cleanup of the associated data.
Just like with bio_resolve_handle, always check for a NULL
return and make it noop.
|
inlinestatic |
Compare two handles.
const bio_tag_t * bio_handle_info | ( | bio_handle_t | handle | ) |
Retrieve the associated tag of a handle.
This should only be used for debugging.
bio_handle_t bio_make_handle | ( | void * | obj, |
const bio_tag_t * | tag | ||
) |
Create a new handle.
A handle is associated with a tag. To retrieve the associated pointer, the same tag must be provided. This is a form of runtime type checking.
For compile time, it is recommended to wrap the returned bio_handle_t
value in a struct:
In fact, this is how bio exposes all of its handles.
obj | The object associated with the handle |
tag | The "type" tag for the handle |
void * bio_resolve_handle | ( | bio_handle_t | handle, |
const bio_tag_t * | tag | ||
) |
Resolve a handle, returning the associated pointer.
If the provided tag
is not the same as what was previously passed to bio_make_handle, this will return NULL
.
If bio_close_handle was called on the handle, this will return NULL
.
A function accepting handle should always check for NULL
and become noop on a NULL
pointer. This is how all bio functions behave.
Example: