Skip to content

fosskers/mongoose

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

38 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Mongoose

This library contains Common Lisp bindings to the Mongoose web server library written in C. Internally Mongoose is event based and so can handle decent concurrent load, but it is otherwise single-threaded in nature.

This library has no Common Lisp dependencies, and by default Mongoose itself only requires openssl.

Table of Contents

Compatibility

The FFI bindings are hand-written to ensure minimal overhead and dependency load. Currently only SBCL is supported.

CompilerCompiles?
SBCL
ECL
Clasp
ABCL
CCL
Clisp
Allegro
LispWorks

Installation

This library requires Mongoose to be available on your system.

Arch Linux

Mongoose is available from the AUR.

aura -A mongoose

MacOS

Mongoose is available with Homebrew.

brew install mongoose

From Source

By design Mongoose is simple and has no complex build process. Simply acquire its source code, compile a .so, and make it available as /usr/lib/libmongoose.so.

git clone https://github.com/cesanta/mongoose.git --depth=1
cd mongoose
gcc -shared -fPIC -o libmongoose.so mongoose.c

and then something like:

cd /usr/lib
sudo ln -s /path/to/libmongoose.so .

Usage

See also the official Mongoose Documentation and the examples directory.

As a rule, the functions and types provided here have the same names as their underlying C ones, except that the mg_ prefix has been stripped and underscores are now dashes (e.g. mg_http_serve_dir -> http-serve-dir).

(in-package :mongoose) has been used for brevity in the examples below, but it’s assumed that you’ll use a package nickname in your own code, perhaps mg.

Note also that for simplicity, no higher level Lisp API has been provided on top of Mongoose itself. We will occasionally be using FFI primitives directly, but you will see that this is not a stressful activity. Some small helper functions have been provided where appropriate.

To run the examples, first:

(asdf:load-system :mongoose)
(asdf:load-system :html)  ; For examples that need it.

Then open the example you want to run, compile/load just that file, and execute its “main” through the REPL.

HTTP Server

Handlers and the Polling Loop

I could have provided some macro-based defhandler, etc., but I realized that such an interface already exists: the FFI itself, which we will embrace in the spirit of minimalism.

Mongoose operates in a single-threaded “polling loop”.

(loop (mgr-poll mgr 1000))

Where mgr is an underlying mg_mgr you’ve already initialized, the 1000 milliseconds is a timeout for accepting incoming connections, and mgr-poll itself is just a direct call to mg_mgr_poll.

Your “main” will look something like this:

(let ((mgr (make-alien mgr)))
  (mgr-init mgr)
  (let ((handler (alien-sap (alien-callable-function 'ev-handler))))
    (http-listen mgr "http://localhost:8000" handler nil))
  (loop (mgr-poll mgr 1000))
  (mgr-free mgr)
  (free-alien mgr))

Of note:

  • make-alien: this initializes some memory that we immediately populate with mgr-init.
  • (alien-sap (alien-callable-function ...)): Mongoose needs a C-level function pointer to its main event handler, and this is how we produce one from the Lisp side.

But what is 'ev-handler referring to? And how can we pass a pointer to a C function when we aren’t writing C? The answer is define-alien-callable.

(define-alien-callable ev-handler void ((c (* connection)) (ev int) (ev-data (* t)))
  "Handle HTTP events."
  (when (= ev +ev-http-msg+)
    (let ((hm (cast ev-data (* http-message))))
      ;; Do whatever!
      (foo hm))))

As you’ll later see in the Examples, our handler will always take this basic shape. define-alien-callable enables us to write whatever Lisp we want for our handler logic, and have C call into it. It even supports hot reloading as usual; our server can already be running, we can hot recompile just this handler, and the changes will be immediately reflected.

You can see that the input and output types of the handler are C types. The (* connection) syntax (etc.) is provided by sb-alien, the main FFI module within SBCL. You can also see us doing a Pointer Cast via cast to get access to the request payload - this is standard practice with Mongoose.

If you need access to the fields of C structs, use slot as can be seen in this example.

Serving Static Files

We get this for free via http-serve-dir.

See examples/00-static-files.lisp.

Routing

Keeping it simple, routing is a matter of extracting the uri field of a http-message and doing string matching on it.

See examples/01-routing.lisp.

Query Parameters

See examples/03-query-params.lisp.

HTML

You’re free to use any HTML library you wish, provided it eventually yields a concrete string to pass through the FFI. For a solution with zero transitive dependencies, consider html.

See examples/02-html.lisp.

Headers

It is a general requirement of HTTP that headers be postfixed by \r\n. For single headers you can do this yourself like so:

(defparameter *content-type-text* (format nil "Content-Type: text/plain~c~c" #\return #\linefeed))

And for multiple via the headers helper:

(in-package :mongoose)
(headers '("Content-Type: text/plain" "User-Agent: foo"))
Content-Type: text/plain
User-Agent: foo

Either way, you’re encouraged to preallocate common headers via a defparameter as shown above.

If no headers are necessary, Mongoose also accepts nil (NULL in C).

(http-reply c 404 nil "")

TLS (HTTPS)

⚠ Coming soon! ⚠

Not yet possible until the recent struct-by-value work in SBCL is released. Mongoose’s TLS functions have one specific place, related to their internal string type, where this is necessary.

Logging

Mongoose does its own logging, which you will see if running an actual Lisp program (but not through the REPL). By default many messages are printed - enough to lower latency. To change this, simply alter the value of *log-level*.

(in-package :mongoose)

(let ((mgr (make-alien mgr)))
  (setf *log-level* +ll-error+)
  (mgr-init mgr)
  ;; ... etc. ...
  (mgr-free mgr)
  (free-alien mgr))

The available log levels are:

  • +ll-none+
  • +ll-error+
  • +ll-info+
  • +ll-debug+ (the default)
  • +ll-verbose+

Error Handling

See examples/04-errors.lisp.

Performance

Measurements

See sample.c for a trivial baseline server and repl.lisp for its Common Lisp analogue. The following command:

echo "GET http://localhost:8000/html" | vegeta attack -duration=10s -rate=0 -max-workers=4 | tee results.bin | vegeta report

produces these results on my 2018 ThinkPad (I write this in January 2026). The only attempt to optimize either code was to disable logging.

LanguageRequests-per-Second
Lisp23600
C25000

There are some string-related inefficiencies in the Lisp that could easily be worked around in production code, thus I am willing to claim that for practical purposes the FFI can be trusted not to introduce undue overhead.

Further Work

Anything else is essentially unplanned but will be considered if there is sufficient demand.

See Also

  • Woo: libev-based web server.
  • Radiance: Full web application framework.

About

Common Lisp bindings to the Mongoose C webserver.

Topics

Resources

License

Stars

Watchers

Forks

Contributors