Skip to content

nasstymatt/mhd2_framework

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

47 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

mhd2_framework

A minimal web "framework" (thin wrapper) for C3, built around libmicrohttpd2, SQLite, and Lua templates.

Not a library nor a fully functional framework. This is just my way of doing things. Copy it, modify it, own it.

NOTE: libmicrohttpd2 is experimental. You might need to debug some build issues manually.

What it is

A working foundation for SSR web apps in C3. Compiles to a single static binary.

2.9MB stripped binary including Lua VM, SQLite, embedded assets and templates.

What's inside

  • Router — middleware chain, route params, static file serving
  • SQLite macro layer — compile-time reflection struct mapping:
TierList[] lists = sqlite3::@query_all{TierList}(db, "SELECT * FROM tierlists");
  • Lua template engineextends, block, include, hot reload in dev, per-thread Lua VM, file-watch cache invalidation.
  • Request lifecycle — arena-per-request memory, multipart file uploads, form, headers, query parsing.
  • MHD2 wrapper — worker thread pool, clean separation from router.

Basic Example

Define a model:

struct Post
{
  long   id;
  String title;
  String body;
  String created_at;
}

Query and render:

module mymodule;

import router;
import templates;
import sqlite3;

SQlite3* db;

fn void init_db() @local @init
{
  if (sqlite3::open_v2("./app.db",
                       &db,
                       OpenFlags.CREATE | OpenFlags.READWRITE | OpenFlags.FULLMUTEX,
                       null
  ) != 0)
  {
    unreachable("DB error: unable to open database: %s", sqlite3::errmsg(db));
  }

  sqlite3::busy_timeout(db, 5000);
  sqlite3::@exec_ok(db,
                "PRAGMA journal_mode=WAL;"
                "PRAGMA synchronous=NORMAL;"
                "PRAGMA foreign_keys=ON;"
                "PRAGMA temp_store=MEMORY;");

  sqlite3::@transaction(db)
  {
    // Init, schema, migrations, etc.
    // sqlite3::@exec(db, $embed("../../sql/schema.sql"))!!;
    // sqlite3::@exec(db, $embed("../../sql/triggers.sql"))!!;
  }!!;
}

fn void close_db() @local @finalizer
{
  if (db == null) return;
  sqlite3::close(db);
  sqlite3::free(null);
}

fn void main()
{
  Router r;
  defer r.free();

  // Middleware receives Context* for pipeline control
  r.use(fn (Context* ctx)
  {
    io::printn(ctx.req);
    ctx.next()!;
  });

  // Route handlers receive (Response* w, Request* r)
  r.get("/posts", fn (Response* w, Request* r)
  {
    Post[] posts = sqlite3::@query_all{Post}(db, "SELECT * FROM posts ORDER BY created_at DESC")!;
    templates::@render(w, "posts.html", posts)!;
  });

  r.get("/posts/:id", fn (Response* w, Request* r)
  {
    String id   = r.@param("id")!;
    Post post   = sqlite3::@query_one{Post}(db, "SELECT * FROM posts WHERE id = ?1", id)!;
    templates::@render(w, "post.html", post)!;
  });

  r.post("/posts", fn (Response* w, Request* r)
  {
    String title = r.form_data["title"] ?? "";
    String body  = r.form_data["body"]  ?? "";
    if (title.trim() == "") return w.error(400, "Title is required");
    long id = sqlite3::@insert(db, "INSERT INTO posts(title, body) VALUES(?1, ?2)", title, body)!;
    w.@redirect("/posts/:id", id);
  });

  r.handle_static("web/assets", "/assets", $feature(EMBED_ASSETS));

  router::microhttpd2::listen_and_serve(8080, &r)!!;
}

Template (posts.html):

{% extends("base.html") %}
{% block("body") %}
<main>
  <h1>Posts</h1>
  {% for _, post in ipairs(posts) do %}
  <article>
    <h2><a href="/posts/{{ post.id }}">{{ post.title }}</a></h2>
    <p>{{ post.created_at }}</p>
  </article>
  {% end %}
</main>
{% endblock() %}

Base layout (base.html):

<!DOCTYPE html>
<html>
  <head><title>My App</title></head>
  <body>
    {% yield("body") %}
  </body>
</html>

Struct fields map to columns by name at compile time — no codegen, no runtime reflection, no annotations.

Compile templates:

lua scripts/tpl_transpile.lua

Or embed templates into binary at compile time:

c3c build --trust=full -D EMBED_TEMPLATES

And static assets:

c3c build --trust=full -D EMBED_TEMPLATES -D EMBED_ASSETS

Template rendering

Route handlers render via templates::@render — variables are passed by name using C3's compile-time $nameof:

// Packed args — variable names become Lua variable names
templates::@render(w, "post.html", post, page, total_pages)!;

// Key-value pairs — explicit names
templates::@render_kv(w, "post.html", "err_title", "Title is required")!;

Building

make        # build deps + dev binary
make release  # optimized build, stripped SQLite and MHD2
make dev      # app only, skip dep check
make clean
make clean-deps

The Makefile builds all three C dependencies from source and links them statically.

Dev build — full debug info, all features enabled, easier to iterate.

Release build — MHD2 compiled with --enable-compact-code and --disable-messages, SQLite compiled with unused features stripped at the preprocessor level: no UTF-16, no window functions, no CTEs, no BLOB literals, no incremental I/O, no trace hooks, no authorization callbacks, and more — only what this app actually uses.

The result is a single statically linked ELF with no external dependencies at runtime.

Stack

Layer Technology
HTTP libmicrohttpd2
Templates Lua 5.4 (transpiled from HTML)
Database SQLite (WAL mode)
Language C3
Frontend HTMX + Alpine.js + Pico CSS

Included app

A tier list application — drag-and-drop image ordering across tiers, file uploads, fractional position algorithm, HTMX partial updates. Used as a real-world integration test for the framework.

Getting started

# Clone modules and third_parties
git clone --recurse-submodules --depth 1 --shallow-submodules https://github.com/nasstymatt/mhd2_framework.git
make release
./build/tierlister-release
# copy src/router, src/db, src/templates into your project
# modify anything

See src/main.c3 for a full example.

Status

Personal/experimental. MHD2 is pre-release.

Use in production at your own risk — or better, read the source first.

Platform: Linux (glibc) & OSX. Tested on x86-64 & arm64.

Protocol: HTTP/1.1 only — HTTP/2 is disabled in the MHD2 build.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors