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:

  1. 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.

  2. 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.

  3. 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:

  • %d matches any digit;

  • [abc]* matches any string consisting of letters a, b, and c;

  • ....? 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:

  • uri contains the full path and query param string.
    • /page/1/2024-12?order=asc

  • path contains just the path.
    • /page/1/2024-12

  • query_count contains the number of query params.
    • 1

  • query contains 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: chunked is automatically included in the response and a special terminating chunk is automatically issued when finished.

  • HTTP/2. Native DATA frames are issued. An END_STREAM flag 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:

  1. Each mn_server instance 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.

  2. Your mn_route_handler_t handler 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.