The C programming language is the grand old man of systems programming. It is undeniably that C makes the world go around, being the implementation language for such “minor” things as the Linux kernel, sqlite, and many programming language runtimes.
Although C was used in the early web through Common Gateway Interface, it has never been the go-to language for web development, especially not on the front end!
Compared to JS WebAssembly provides a much lower level virtual machine in the browser that can be used as a compilation target for any native code. There are many languages that have a WebAssembly port. It is time to try making a client-side web app in (mostly) pure C. In this post, we will make the traditional simple to-do-list application.
HTML generation
The first obvious step is generating some HTML. There are text-templating solutions for C, but I want to avoid any dependencies so we’ll roll our own.
We can have a static buffer that all rendering is done to:
// I have it on good authority that 640k ought to be enough for anyone
#define HTML_MAX_MEMORY 640 * 1024
char _html_memory[HTML_MAX_MEMORY];
char *_html_out;
Then we can have utilities to output things like tag start, attributes, and tag end.
void tag_start(const char *name);
void tag_end(const char *name);
void attr_s(const char *name, const char *value);
void attr_i(const char *name, int value);
void attr_b(const char *name);
We implement these as calls to snprintf
to output formatted text into the buffer
without going over the statically allocated memory limit.
Then we can finish things by adding some convenience macros:
#define tag(name, body) \
{ \
tag_start(name); \
body tag_end(name); \
}
This allows us to write HTML generation code that looks like this:
tag("div", {
attr_s("class", "todo-form");
tag("input", {
attr_s("placeholder", "What needs to be done?");
attr_s("onchange", "add_todo(event.target.value); event.target.value='';");
});
});
It ain’t exactly pretty like hiccup or JSX,
but if you squint hard enough you can kind of see the HTML structure.
You can see the HTML-generating code on GitHub: html.c
Data model and state management
Next up is modeling and managing the data. What we want again is something simple, especially as we need to manage memory manually.
We can model the Todo
as C struct, containing the index, label, and completion
status of the item as well as links to previous and next entries.
We could use a dynamic array
to store the data, but again we will have a statically allocated pool with 1024
entries. If anyone has more things on their to-do-list, they need better software to
manage it anyway!
typedef struct Todo Todo;
struct Todo {
int idx;
char label[255];
bool complete;
Todo *prev;
Todo *next;
};
#define MAX_TODOS 1024
Todo todo_memory[MAX_TODOS]; // memory holding the todos
Todo *todos = NULL; // list of active todos
Todo *last_todo = NULL; // the last todo, for adding to end
Todo *free_todos = NULL; // list of free todos
int num_todos = 0; // number of active todos
The purpose of having the index is that we can modify any item by just looking
it up from the todo_memory
and manipulating it. The doubly linked list is to
maintain the order.
We initially add all todos to the free_todos
list. When we add a todo we take the
first one from that list and add it to the active todos
list.
The “API” that we need for JS handlers is:
- adding a todo
- deleting a todo
- toggling a todo’s completion.
After any change from JS side, we rerender the application.
Toggling is easy, we can just access the memory by index:
void toggle_todo(int idx) {
if(valid_idx(idx)) todo_memory[idx].complete = !todo_memory[idx].complete
rerender();
}
Adding a new todo is a little more involved:
void add_todo(char *data) {
Todo *t = free_todos;
if(t == NULL) {
printf("No more free todos!\n");
} else {
free_todos = t->next;
t->next = NULL;
if(todos == NULL) { // first todo
todos = t;
} else {
last_todo->next = t;
t->prev = last_todo;
}
last_todo = t;
// copy to our own memory
size_t len = strlen(data);
len = len > 255 ? 255 : len;
memcpy(&t->label[0], data, len);
t->label[len] = 0;
t->complete = false;
t->next = NULL;
rerender();
}
}
Deletion returns the Todo to the front of the free_todos
list.
You can view the full code on GitHub: todo.c
Compiling and integrating
After we have rendering and data management we are ready to hook this up to a page and have some JS bindings.
For compilation we use emscripten which will also create a host HTML page based on our shell.
$ emcc -o todo.html \
todo.c html.c \
-s EXPORTED_FUNCTIONS=_init,_add_todo,_delete_todo,_toggle_todo,ccall \
--no-entry \
--shell-file shell.html
This will compile our app and create 3 files:
todo.js
contains emscripten JS integration codetodo.wasm
the compiled WebAssembly binarytodo.html
HTML page for the application
In our shell.html
we add some
styling and have a div with id app
which the application will render to once loaded.
We also add JS wrappers to call our C-side functions.
function add_todo(label) {
Module.ccall("add_todo", null, ["string"], [label]);
}
See emscripten documentation for interacting with code for more details.
Once the app is compiled, we can serve it locally with a simple HTTP server.
Further work
There’s lots more that could be done, but I’ll leave these as an exercise for the reader.
TodoMVC
The app could be styled according to the TodoMVC app and the full functionality added.
Persistent storage
You could load and save the todo’s into client side IndexedDB storage using emscripten File System API.
More fine grained updates
The current implementation simply rerenders the whole app and sets the innerHTML
of the
application element. You could update only changed items (think implementing some form of virtual DOM
in C). Alternatively you could just use a library like Idiomorph
to mutate the DOM.
Final remark
Should you start doing front-ends with C? Probably not, but this was a fun experiment. Emscripten itself is a robust tool and WebAssembly looks like it could become a sort of universal bytecode.
Even if you won’t do front-end work in C, there are so many languages that compile to C or have an implementation in C that WebAssembly can help reach the browser.
Happy hacking!