Concepts¶
Though makinori is intended to feel familiar to even first time users, there remain certain concepts that are less common and worth expanding on.
Routing¶
A client request is compared against a number of
user-defined routes. These specify a method field and pattern field to compare the
request path against. Routes are arranged as a linked list and traversed in
order until a match is found. Consider the following snippet:
static struct mn_route route_first;
static struct mn_route route_second;
static struct mn_route route_third;
static struct mn_route route_first = {
.method = MN_METHOD_GET,
.pattern = mn_str_lit("/first"),
.handler = handle_first,
.next = &route_second};
static struct mn_route route_second = {
.method = MN_METHOD_GET,
.pattern = mn_str_lit("/second"),
.handler = handle_second,
.next = &route_third};
static struct mn_route route_third = {
.method = MN_METHOD_GET,
.pattern = mn_str_lit("/third"),
.handler = handle_third};
struct mn_server server = {.config = config, .route = route_first};
mn_server_run(&server);
In this example, the server starts running and waits
on the configured port for a client request. Once received, the HTTP method and
path declared in the request is compared against the method and pattern
specified in route_first. If both equal, the user-defined handle_first
method is invoked . Otherwise next is traversed and the process repeats. If
no match is found, makinori automatically returns an HTTP 404 Not Found.
Tip
The forward declarations made at the top of the snippet introduce a small
readability improvement, letting us define mn_route instances in
the order they would be traversed. Otherwise they must be listed in reverse
order.
Many other frameworks introduce a means of defining routes hierarchically whereas makinori shys away from this for a few reasons:
Designing an ergonomic, non-macro-heavy, compile-time interface of a URL hierarchy is challenging. Because of limitations in C, a hard-coded limit to e.g. nesting depth must be introduced at some point.
Though URL hierarchies match the intuition one probably has around resource nesting, they also end up being quite prescriptive. Often times one is forced to rewrite URLs to workaround typical hierarchical matching algorithms.
For those who do want a different interface, a simple linked list is an easy target for transforming any URL configuration into.
Patterns¶
In the snippet above, paths of incoming requests have to equal a route’s
pattern field exactly to constitute a match. By leveraging
patterns, we can generalize what dictates a match. For example:
%dmatches any digit;[abc]*matches any string consisting of lettersa,b, andc;....?matches any sequence of three or four characters.
In fact, Lua’s pattern matching mechanism is used directly, so any pattern
supported by Lua is suitable for use. Keep in mind, the version of Lua used
during compilation may dictate what patterns are available to you. Also note
that the / character found in URIs has no special status. This means a
pattern like /.* will match every route.
Note
makinori automatically introduces leading anchor ^ and trailing
anchor $ to a pattern if it does not include them.
Tip
If the presence or absence of a trailing slash on a path should be treated
equivalently, include a /? suffix to the end of your patterns.
Captures¶
Since Lua’s pattern matching facilities are imported wholesale, it should be no
surprise that captures are also supported. Captures allow easily referencing
certain parts of a path. For example, suppose the following route matched
against path /page/14/2024-12:
static struct mn_route route_page = {
.method = MN_METHOD_GET,
.pattern = mn_str_lit("/page/(%d+)/(%d%d%d%d)-(%d%d)"),
.handler = handle_page};
Each parenthesized group denotes a capture of which this particular
pattern defines three. The handle_page handler will be given an
mn_request, say req, satisfying req.capture_count == 3 and
req.captures[0] with value
14;req.captures[1] with value
2024;req.captures[2] with value
12.
It is still the user’s responsibility to understand the order of captures and to cast them into different data types if necessary.
Note
Captures can be nested, e.g. /(%d(%d)%d) is a valid pattern. The
order of captures in these situations is dictated by left parentheses.
In particular, the first left parenthesis found is that corresponding to
three enclosed %d symbols. The second is that corresponding to the
singular %d symbol. Therefore a path of e.g. /123 will result in
req.captures[0] having value 123 and req.captures[1] having value
2.
Handlers¶
As briefly described above, every mn_route has an associated
handler triggered on match. A handler is intentionally
very simply defined:
struct mn_status mn_route_handler_t( \
struct mn_request const, \
struct mn_response *const)
That is to say, a handler is any function that takes in a request and a response. It also returns an
mn_status. If the handler returns a failing status, the connection
is immediately terminated.
Requests¶
Unsurprisingly, a request object contains
details surrounding a client’s request. A request of form e.g.
/page/1/2024-12?order=asc is decomposed into the following fields:
uricontains the full path and query param string./page/1/2024-12?order=asc
pathcontains just the path./page/1/2024-12
query_countcontains the number of query params.1
querycontains the parsed query params.{ .key = "order", .value = "asc" }.
Captures are also included in the request if relevant. These were covered earlier.
Responses¶
makinori defaults to streaming HTTP responses to the client when
possible. When writing to a mn_response object using methods like
mn_response_set_code() or mn_response_write(), you are also
writing a response directly to the client (outside of a small buffer period).
This feature happens transparently depending on which protocol the client
requested:
HTTP/1.0. The connection is terminated once the response is finished streaming to indicate the end of the content.
HTTP/1.1. Header
Transfer-Encoding: chunkedis automatically included in the response and a special terminating chunk is automatically issued when finished.HTTP/2. Native
DATAframes are issued. AnEND_STREAMflag is automatically sent in the final frame.
As a consequence, you must finish writing the HTTP headers to the response
before you begin writing the body. Since we default to streaming, it is not
necessary to provide a Content-Length flag in your responses. You can if you
need to, but the value will need to be calculated and set before invoking any
method that writes to the body.
Event Loop¶
Every mn_server instance runs an event loop. For the most part, this
should be a relatively transparent feature of makinori, but there are a few
caveats that should be considered:
Each
mn_serverinstance must reside in its own thread. To be clear, you can run multiple threads without issue. But, within any particular response handler, you must not spawn a new thread. To do so invokes the wrath of undefined behavior.Your
mn_route_handler_thandler functions must work together. Avoid hogging the CPU or blocking on I/O since the entire event loop necessarily waits alongside your handler. This is elaborated on below.
The default event loop is managed using poll. We plan on supporting other
event loops in the future (namely those already supported by libwebsockets),
but doing so is not a priority. Please file an issue if the need arises.
Coroutines¶
As it turns out, every mn_route_handler_t defined within an
mn_route runs within its own stackful coroutine. Methods that
perform I/O (e.g. mn_response_write()) will automatically yield control
at opportune moments, relying on the event loop to eventually resume the
suspended handler (ideally once asynchronous I/O operations are finished).
As such, it is important to write cooperative handlers. Avoid locking the CPU
indefinitely or running blocking I/O operations since the entire event loop will
otherwise block as well. For cases where the existing API falls shorts, you can
voluntarily suspend your coroutine using mn_response_suspend(). It is up
to the internal scheduler to eventually resume your handler.