Introduction to Stencil

Stencil is a library for developing web-based applications in C++. It includes request and response objects, a skinnable template engine, server api abstraction, and facilities for load balancing, browser jogging, security and catching runaway CGI's. It also includes some useful Javascripts and images.


Template Engines

A template engine is a framework for developing web-based applications which emphasizes seperation of code and HTML.

An application that uses a template engine consists of 2 distinct parts: code that performs calculations or looks up data and HTML pages with extra tags for performing substitutions and other very rudimentary operations.

Using a template engine, it should be possble to write a web-based application without embedding code in the HTML and without embedding HTML in the code.

For example:

Web Page: (http://www.mysite.com/addressbook/default/addressbook/addressbook.html)
<html>
<head>
        <title>Address Book</title>
</head>
<body>
        Address Book for $(login):<br><br>
        <table>
        <tr>
        <th>Name</th><th>Address</th><th>City</th>
        <th>State</th><th>Phone Number</th>
        </tr>
        <!-- start addressentry -->
        <tr>
        <td>$(name)</td><td>$(address)</td><td>$(city)</td>
        <td>$(state)</td><td>$(phonenumber)</td>
        <tr>
        <!-- end addressentry -->
        </table>
</body>
</html>
Code: (http://www.mysite.com/addressbook/addressbook.cgi)
// Copyright (c) 2004 David Muse
// See the COPYING file for more information

#include <stencil/templateengine.h>
#include <stencil/request.h>
#include <stencil/response.h>
#include <rudiments/charstring.h>
#include <rudiments/stringbuffer.h>


// handler for login.html
bool loginhtml(inputoutput *io, request *req, response *resp,
                        stringbuffer *container, const char *filename,
                        void *data) {

        // send an http header
        resp->textHtml();

        // parse page contents
        return templateengine::parseFile(io,req,resp,NULL,req->pagePath(),
                                                                NULL,NULL,NULL);
}


// handler for addressentry segment
bool addressentry(inputoutput *io, request *req, response *resp,
                        stringbuffer *container, const char *name,
                        const char *segment, uint64_t length,
                        void *data) {

        struct addressdata {
                char    *name;
                char    *address;
                char    *city;
                char    *state;
                char    *phonenumber;
        };

        // build a list of names and address data
        // normally you'd look this data up in a database, but for this
        // example, it's hardcoded into the program
        addressdata     ad[]={
                {"David","1234 Dave's St.","Cumming","GA","555-1234"},
                {"Mark","2345 Mark's St.","Baton Rouge","LA","555-2345"},
                {"Nick","3456 Nick's Rd.","Some City","NJ","555-3456"},
                {"Mike","4567 Mike's St.","Houston","TX","555-4567"}
        };

        // for each address in the list, parse the segment,
        // substituting in the name and address data
        for (int i=0; i<4; i++) {

                variable        var[]={
                        {"name",ad[i].name},
                        {"address",ad[i].address},
                        {"city",ad[i].city},
                        {"state",ad[i].state},
                        {"phonenumber",ad[i].phonenumber},
                        {NULL,NULL}
                };

                if (!templateengine::parseSegment(io,req,resp,container,
                                                segment,length,NULL,NULL,var)) {
                        return false;
                }
        }
        return true;
}


// handler for addressbook.html
bool addressbookhtml(inputoutput *io, request *req, response *resp,
                        stringbuffer *container, const char *filename,
                        void *data) {

        // validate login
        // normally you'd look this data up in a database, but for this
        // example, it's hardcoded into the program
        if (charstring::compare(req->formEntry("login"),"myuser") ||
                charstring::compare(req->formEntry("password"),"mypassword")) {
                return false;
        }

        // send an http header
        resp->textHtml();

        // use built-in substitution variables (form entries, cookies, etc.)
        variables vars[]={
                {req->allVariables(),req->allValues()},
                {NULL,NULL}
        };

        // handle the segments in the page
        segmenthandler  sh[]={
                {NULL,"addressentry",addressentry,NULL},
                {NULL,NULL,NULL,NULL}
        };

        // parse page contents
        return templateengine::parseFile(io,req,resp,NULL,req->pagePath(),
                                                                sh,vars,NULL);
}


#include <stencil/cgimodule.h>

MAIN {

        inputoutput     io(APISTRUCT);
        request         req(&io);
        response        resp(&io);

        // associate pages with page handling functions
        pagehandler     ph[]={
                {"addressbook","login.html",loginhtml,NULL},
                {"addressbook","addressbook.html",addressbookhtml,NULL},
                {NULL,NULL,NULL,NULL}
        };

        // handle the page
        if (templateengine::handlePage(&io,&req,&resp,ph,req.module(),
                                                req.page(),req.pagePath())) {
                EXIT(0);
        }

        // if an error occurred, display the error page
        stringbuffer    filename;
        filename.append(req.skinPath())->append("/error/error.html");
        resp.textHtml();
        templateengine::parseFile(&io,&req,&resp,NULL,
                                        filename.getString(),NULL,NULL,NULL);
        EXIT(1);
}
Output: (http://www.mysite.com/addressbook/addressbook.cgi/default/addressbook/addressbook.html)
<html>
<head>
        <title>Address Book</title>
</head>
<body>
        Address Book for myuser:<br><br>
        <table>
        <tr>
        <th>Name</th><th>Address</th><th>City</th>
        <th>State</th><th>Phone Number</th>
        </tr>

        <tr>
        <td>David</td><td>1234 Dave's St.</td><td>Cumming</td>
        <td>GA</td><td>555-1234</td>
        <tr>

        <tr>
        <td>Mark</td><td>2345 Mark's St.</td><td>Baton Rouge</td>
        <td>LA</td><td>555-2345</td>
        <tr>

        <tr>
        <td>Nick</td><td>3456 Nick's Rd.</td><td>Some City</td>
        <td>NJ</td><td>555-3456</td>
        <tr>

        <tr>
        <td>Mike</td><td>4567 Mike's St.</td><td>Houston</td>
        <td>TX</td><td>555-4567</td>
        <tr>

        </table>
</body>
</html>

Skinnable Template Engines

These days, lots of applications are skinnable or themeable. It's possible to make many applications look very different by changing the shape, color and layout of the controls while the underlying functionality of the application remains the same.

When using a template engine, the set of HTML pages associated with an application can collectively be considered it's skin. An application can be directed to use one set of pages or another, in effect, changing it's skin.

An application may have a default skin, a low-graphics skin for dial-up users, a skin with small graphics for PDA's, etc.

Another application of skins is co-branding. A service provider might host the same application for multiple clients. For each client, the application might be skinned to look exactly like the client's web site. The client could link from his site over to the application and still maintain a consistent experience for the user.


Server API Abstraction

Web-based applications can be written as a collection of standalone CGI's or as server modules. Server modules tend to be more efficient but some web servers (ie. on embedded platforms) don't support server modules.

Stencil allows you to compile the same code as a server module or CGI using different compile-time switches. Currently Apache 1.3 and 2.0 server modules are supported.


Stencil's Origins

Stencil was originally written in late 1998 under the name Groundwork and was first released in mid 1999. The terms "template engine" and "skin" hadn't been coined yet, or if they had, they weren't in common use.

At the time, Perl scripts that generated HTML were old-school. The new idea was to embed code directly in HTML using PHP or ASP. Stencil (then Groundwork) took a different approach and met much resistance. Since then, dozens of template engines have popped up and the approach is becoming more and more standard.

While it got the job done, Groundwork was rather clumsy. In comparison, Stencil is faster, lighter, more modern and much more flexible than Groundwork with regard to application structure.


Why C++?

When I first wrote Groundwork the target platform was a 150 mhz Pentium 1 with 32mb of ram running SCO OpenServer 5 and a Netscape web server and it needed to serve hundreds of pages per second. Later on a 600 mhz Pentium 2 with 256mb of ram we needed to serve thousands of pages per second.

At the time, C or C++ was really the only choice. I like object-oriented development so I went with C++. Perl, PHP and other scripting languages used too much ram and/or didn't perform well. Java hadn't made much headway on the server side yet. Since then, people have written template engines for every other language so there's no immediate need to port Stencil to another language.

These days, Stencil fills the small-footprint/high-performance niche. It's suitable for use on embedded platforms and performs very well on more powerful systems.

If you would like to contribute to this project, please contact david.muse@firstworks.com.