Skip to content

tinyweb for C

tinyweb’s C implementation is a library of its own: a C program can use it to draw on a web page, add buttons, labels and sliders to it, play sounds and answer the reader, with no Pyking anywhere. It is the file Pyking copies beside a program compiled for c23, renamed tinyweb.c, tinyweb.h.

Download tinyweb-c.zip: tinyweb.c, tinyweb.h, the example below, and the samples in tinyweb_assets/.

Running a program

The program serves its own page: web_init starts the server and prints its address, and once the program’s own code has finished it goes on serving the page and calling the functions it handed over until it is stopped (Ctrl-C). Open the address in a browser.

Terminal window
cc -o example example.c tinyweb.c
./example

The page asks for a sample beside where the program runs first, then in tinyweb_assets/ there.

An example

/* A canvas, two circles, two samples and buttons that play them, written
in C against tinyweb.c. Build it with
cc -o example example.c tinyweb.c
then run ./example and open the address it prints. */
#include <stdio.h>
#include "tinyweb.h"
static int kick;
static int snare;
static int presses = 0;
static void play_kick(void)
{
tinyweb_play_sample(kick, 0.0, 1.0, 1.0);
}
static void play_snare(void)
{
presses += 1;
printf("snare, press %d\n", presses);
tinyweb_play_sample(snare, 0.0, 0.5, 1.0);
}
static void nothing(void)
{
}
int main(void)
{
int canvas;
tinyweb_web_init(0);
kick = tinyweb_load_sample("kick1.wav");
snare = tinyweb_load_sample("snare1.wav");
canvas = tinyweb_canvas(400, 200);
tinyweb_draw_circle(canvas, 100, 50, 30, "#ff0000");
tinyweb_draw_circle(canvas, 100, 50, 20, "#000000");
tinyweb_make_button("Play kick", play_kick);
tinyweb_make_button("Play snare", play_snare);
tinyweb_make_button("Does nothing", nothing);
printf("built the page\n");
return 0;
}

Functions

Every function but web_init needs web_init to have been called first, or PYKING_TINYWEB_LOG to be set (below); otherwise it says so and the program ends with status 1.

web_init

void tinyweb_web_init(int port);

Serves the program’s page on port and prints its address, tinyweb: http://127.0.0.1:N/; port 0 picks any free port. Call it before anything else. With PYKING_TINYWEB_LOG set it does nothing.

canvas

int tinyweb_canvas(int width, int height);

A new canvas, width by height pixels, placed below what the page already shows. Answers the number that names it.

fill_canvas

void tinyweb_fill_canvas(int c, const char *color);

Fills the whole canvas c with color, a CSS color such as "#ff0000" or "red".

draw_circle

void tinyweb_draw_circle(int c, int x, int y, int radius, const char *color);

A filled circle on the canvas c, centred at (x, y).

draw_rect

void tinyweb_draw_rect(int c, int x, int y, int width, int height, const char *color);

A filled rectangle on the canvas c, with its top left corner at (x, y).

draw_line

void tinyweb_draw_line(int c, int x1, int y1, int x2, int y2, const char *color, int width);

A straight line on the canvas c, from (x1, y1) to (x2, y2), width pixels wide.

draw_text

void tinyweb_draw_text(int c, int x, int y, const char *words, const char *color, int size);

words written on the canvas c, with the top left corner of the text at (x, y), size pixels high.

make_button

int tinyweb_make_button(const char *title, void (*on_click)(void));

A button showing title, placed below what the page already shows, which calls on_click each time it is pressed. Answers the number that names it.

label

int tinyweb_label(const char *text);

A line of text, placed below what the page already shows. Answers the number that names it to set_label.

set_label

void tinyweb_set_label(int id, const char *text);

Replaces the text of the label id with text.

make_slider

int tinyweb_make_slider(int low, int high, int value, void (*on_change)(int));

A slider from low to high, starting at value, placed below what the page already shows, which calls on_change with each new value it is moved to. Answers the number that names it.

apply_style

void tinyweb_apply_style(int id, const char *kind, const char *property, const char *value);

Changes the page’s look. With kind “theme”, value names a theme: “light” (the default), “dark”, “plain”, “paper” or “retro”. With kind “page”, “button”, “label”, “slider” or “canvas”, property of every element of that kind becomes value; with kind “element”, only the element id’s. property is one of background, color, border, radius, font, size, padding, gap, accent and width, and value is CSS, such as “#336699” or “12px”. id is 0 except for an element.

load_sample

int tinyweb_load_sample(const char *name);

Starts loading the sound file name and answers a number naming it to play_sample. The name is looked up beside where the program runs first, and then among the samples tinyweb ships in tinyweb_assets/: kick1.wav, snare1.wav, hihat1.wav and piano_c1.wav.

play_sample

void tinyweb_play_sample(int sample, double at, double volume, double rate);

Plays the sample sample: at the time at on the page’s clock, or now if that has passed; volume times as loud, and rate times as fast, which raises its pitch as much.

time

double tinyweb_time(void);

The page’s clock, in seconds, as the page last reported it: inside an on_frame callback, the time the frame is for. It is the audio clock once sound has started, so a time read here is a time play_sample can be given.

on_frame

void tinyweb_on_frame(void (*draw)(double));

Calls draw before each frame the page shows, with the time the frame is for.

on_mouse

void tinyweb_on_mouse(int c, void (*click)(int, int));

Calls click with the position, in pixels from the top left corner, of every click on the canvas c.

on_key

void tinyweb_on_key(void (*pressed)(const char *));

Calls pressed with the name of every key pressed while the page has the focus: a letter or digit as itself, and otherwise the browser’s name for it, such as “ArrowLeft”, “Enter” or ” ”.

Without a page

Two environment variables, each a file path, run a program with no page at all, which is how it is tested: with PYKING_TINYWEB_LOG set, every command the program would send its page is appended to that file as one line, and once the program’s own code has finished the events PYKING_TINYWEB_EVENTS lists — click ID, slide ID VALUE, frame T, mouse ID X Y, key NAME, one per line — are delivered as if a reader had caused them.

Terminal window
PYKING_TINYWEB_LOG=page.log PYKING_TINYWEB_EVENTS=clicks.txt ./example

What Pyking generates

Pyking’s own tinyweb tests, compiled for c23. Each comes with the libraries it needs as Pyking writes them — tinyweb’s implementation under its own name, and the module Pyking compiles from tinyweb’s Pyking half — and is built and run with:

Terminal window
cc -std=c23 -o program tinyweb_external.c tinyweb.c program.c
./program

lib_tinyweb_frames

# Animation kept in time with sound: each frame is drawn for the time
# the page gives it, and each beat is scheduled half a second ahead on
# the same clock, so it sounds where the picture says it is. The
# .events file delivers four frames.
import tinyweb as tw
tw.web_init(0)
c = tw.canvas(200, 100)
kick = tw.load_sample("kick1.wav")
beat = 0.5
var next_beat = 0.0
def draw(t: Float):
tw.clear(c, "#111111")
tw.circle(c, int(t * 100.0), 50, 10, "#ffcc00")
if t >= next_beat:
print("beat scheduled at " ++ string(next_beat + beat) ++ ", drawn at " ++ string(t))
tw.play(kick, at = next_beat + beat)
next_beat := next_beat + beat
tw.on_frame(draw)
print(tw.time())

lib_tinyweb_frames.zip

program.c, as Pyking generates it
// Generated from lib_tinyweb_frames.pyk by the Pyking compiler. Do not edit.
// Target: c23. One translation unit of the program; compile it with the others.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <float.h>
#include "tinyweb.h"
// A Pyking function is a pointer to a C function.
#ifndef PYK_FN_F_V
#define PYK_FN_F_V
typedef void (*pyk_fn_f_v)(double);
#endif
// What the external modules this one imports implement.
void tinyweb_web_init(int);
int tinyweb_canvas(int, int);
int tinyweb_load_sample(const char *);
double tinyweb_time(void);
void tinyweb_on_frame(pyk_fn_f_v);
static int pyk_truncate(double x)
{
if (x != x || x <= -2147483649.0 || x >= 2147483648.0) {
return 0;
}
return (int)x;
}
// Concatenation allocates and never frees: Pyking has no ownership to compile into a free.
static const char *pyk_concat(const char *a, const char *b)
{
size_t la = strlen(a);
size_t lb = strlen(b);
char *result = malloc(la + lb + 1);
memcpy(result, a, la);
memcpy(result + la, b, lb + 1);
return result;
}
// A Float is written by this rather than by a printf conversion: no host spells a double the way Pyking does. It allocates and never frees, as concatenation does.
static const char *pyk_float_str(double x)
{
char buf[64];
char digits[32];
char *out = malloc(64);
double a;
int negative, p, at, i, n, decpt, k;
const char *e;
if (x != x) { strcpy(out, "nan"); return out; }
if (x > DBL_MAX) { strcpy(out, "inf"); return out; }
if (x < -DBL_MAX) { strcpy(out, "-inf"); return out; }
negative = x < 0.0 || (x == 0.0 && 1.0 / x < 0.0);
a = negative ? -x : x;
if (a == 0.0) { strcpy(out, negative ? "-0.0" : "0.0"); return out; }
for (p = 0; p < 16; p++) {
sprintf(buf, "%.*e", p, a);
if (strtod(buf, NULL) == a) break;
}
if (p == 16) sprintf(buf, "%.*e", 16, a);
e = strchr(buf, 'e');
at = (int)(e - buf);
n = 0;
for (i = 0; i < at; i++) if (buf[i] != '.') digits[n++] = buf[i];
while (n > 1 && digits[n - 1] == '0') n--;
digits[n] = '\0';
decpt = atoi(e + 1) + 1;
k = 0;
if (negative) out[k++] = '-';
if (decpt <= -4 || decpt > 16) {
out[k++] = digits[0];
if (n > 1) { out[k++] = '.'; strcpy(out + k, digits + 1); k += n - 1; }
sprintf(out + k, "e%d", decpt - 1);
return out;
}
if (decpt <= 0) {
out[k++] = '0';
out[k++] = '.';
for (i = 0; i < -decpt; i++) out[k++] = '0';
strcpy(out + k, digits);
return out;
}
if (decpt >= n) {
strcpy(out + k, digits);
k += n;
for (i = 0; i < decpt - n; i++) out[k++] = '0';
strcpy(out + k, ".0");
return out;
}
for (i = 0; i < decpt; i++) out[k++] = digits[i];
out[k++] = '.';
strcpy(out + k, digits + decpt);
return out;
}
// What the top level binds, where every function can read it; the top level below is what fills each of them in.
static int c;
static int kick;
static double beat;
static double next_beat;
// Declared ahead of their definitions: a Pyking function may be called before it is written.
static void draw(double t);
static void draw(double t)
{
double tmp1;
tinyweb_clear(c, "#111111");
tinyweb_circle(c, pyk_truncate(t * 100.0), 50, 10, "#ffcc00");
if (t >= next_beat) {
printf("%s\n", pyk_concat(pyk_concat(pyk_concat("beat scheduled at ", pyk_float_str(next_beat + beat)), ", drawn at "), pyk_float_str(t)));
(tmp1 = tinyweb_play__default__volume(), tinyweb_play(kick, next_beat + beat, tmp1, tinyweb_play__default__rate()));
next_beat = next_beat + beat;
}
}
int main(void)
{
tinyweb_init();
tinyweb_web_init(0);
c = tinyweb_canvas(200, 100);
kick = tinyweb_load_sample("kick1.wav");
beat = 0.5;
next_beat = 0.0;
tinyweb_on_frame(draw);
printf("%s\n", pyk_float_str(tinyweb_time()));
return 0;
}

lib_tinyweb_input

# The mouse, the keyboard and a slider, each reported on a label, and
# the other drawing functions. The .events file clicks the canvas,
# presses two keys -- the second Enter -- and moves the slider.
import tinyweb as tw
tw.web_init(0)
c = tw.canvas(300, 200)
tw.rect(c, 0, 0, 300, 200, "#eeeeee")
tw.line(c, 0, 0, 300, 200)
tw.line(c, 0, 200, 300, 0, "red", width = 3)
tw.text(c, 10, 10, "click \"me\"", size = 20)
status = tw.label("nothing yet")
def clicked(x: Int, y: Int):
tw.set_label(status, "clicked at " ++ string(x) ++ ", " ++ string(y))
tw.circle(c, x, y, 5, "blue")
def pressed(key: String):
print("key [" ++ key ++ "]")
if key == "Enter":
tw.play_file("snare1.wav")
def moved(value: Int):
tw.set_label(status, "slider at " ++ string(value))
tw.on_mouse(c, clicked)
tw.on_key(pressed)
tw.slider(0, 10, 5, on_change = moved)
tw.slider(1, 3, 2)

lib_tinyweb_input.zip

program.c, as Pyking generates it
// Generated from lib_tinyweb_input.pyk by the Pyking compiler. Do not edit.
// Target: c23. One translation unit of the program; compile it with the others.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "tinyweb.h"
// A Pyking function is a pointer to a C function.
#ifndef PYK_FN_II_V
#define PYK_FN_II_V
typedef void (*pyk_fn_ii_v)(int, int);
#endif
#ifndef PYK_FN_S_V
#define PYK_FN_S_V
typedef void (*pyk_fn_s_v)(const char *);
#endif
// What the external modules this one imports implement.
void tinyweb_web_init(int);
int tinyweb_canvas(int, int);
int tinyweb_label(const char *);
void tinyweb_set_label(int, const char *);
void tinyweb_on_mouse(int, pyk_fn_ii_v);
void tinyweb_on_key(pyk_fn_s_v);
static const char *pyk_int_str(int n)
{
char *out = malloc(24);
sprintf(out, "%d", n);
return out;
}
// Concatenation allocates and never frees: Pyking has no ownership to compile into a free.
static const char *pyk_concat(const char *a, const char *b)
{
size_t la = strlen(a);
size_t lb = strlen(b);
char *result = malloc(la + lb + 1);
memcpy(result, a, la);
memcpy(result + la, b, lb + 1);
return result;
}
// What the top level binds, where every function can read it; the top level below is what fills each of them in.
static int c;
static int status;
// Declared ahead of their definitions: a Pyking function may be called before it is written.
static void clicked(int x, int y);
static void pressed(const char *key);
static void moved(int value);
static void clicked(int x, int y)
{
tinyweb_set_label(status, pyk_concat(pyk_concat(pyk_concat("clicked at ", pyk_int_str(x)), ", "), pyk_int_str(y)));
tinyweb_circle(c, x, y, 5, "blue");
}
static void pressed(const char *key)
{
printf("%s\n", pyk_concat(pyk_concat("key [", key), "]"));
if (strcmp(key, "Enter") == 0) {
tinyweb_play_file("snare1.wav");
}
}
static void moved(int value)
{
tinyweb_set_label(status, pyk_concat("slider at ", pyk_int_str(value)));
}
int main(void)
{
const char *tmp1;
tinyweb_init();
tinyweb_web_init(0);
c = tinyweb_canvas(300, 200);
tinyweb_rect(c, 0, 0, 300, 200, "#eeeeee");
(tmp1 = tinyweb_line__default__color(), tinyweb_line(c, 0, 0, 300, 200, tmp1, tinyweb_line__default__width()));
tinyweb_line(c, 0, 200, 300, 0, "red", 3);
tinyweb_text(c, 10, 10, "click \"me\"", tinyweb_text__default__color(), 20);
status = tinyweb_label("nothing yet");
tinyweb_on_mouse(c, clicked);
tinyweb_on_key(pressed);
tinyweb_slider(0, 10, 5, moved);
tinyweb_slider(1, 3, 2, tinyweb_slider__default__on_change());
return 0;
}

lib_tinyweb_sound_graphics1

# A canvas, two circles, two samples and buttons that play them: the
# first tinyweb program.
import tinyweb as tw
tw.web_init(0)
kick = tw.load_sample("kick1.wav")
snare = tw.load_sample("snare1.wav")
canvas = tw.canvas(400, 200)
tw.circle(canvas, 100, 50, 30, "#ff0000") # canvas, x, y, radius, color
tw.circle(canvas, 100, 50, 20) # the color defaults to "#000000"
var presses = 0
def play_kick():
tw.play(kick)
def play_sound():
presses := presses + 1
print("snare, press " ++ string(presses))
tw.play(snare, volume = 0.5)
tw.button("Play kick", on_click = play_kick)
tw.button("Play snare", on_click = play_sound)
tw.button("Does nothing") # on_click is optional
print("built the page")

lib_tinyweb_sound_graphics1.zip

program.c, as Pyking generates it
// Generated from lib_tinyweb_sound_graphics1.pyk by the Pyking compiler. Do not edit.
// Target: c23. One translation unit of the program; compile it with the others.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <float.h>
#include "tinyweb.h"
// What the external modules this one imports implement.
void tinyweb_web_init(int);
int tinyweb_canvas(int, int);
int tinyweb_load_sample(const char *);
static const char *pyk_int_str(int n)
{
char *out = malloc(24);
sprintf(out, "%d", n);
return out;
}
// Concatenation allocates and never frees: Pyking has no ownership to compile into a free.
static const char *pyk_concat(const char *a, const char *b)
{
size_t la = strlen(a);
size_t lb = strlen(b);
char *result = malloc(la + lb + 1);
memcpy(result, a, la);
memcpy(result + la, b, lb + 1);
return result;
}
// A Float is written by this rather than by a printf conversion: no host spells a double the way Pyking does. It allocates and never frees, as concatenation does.
static const char *pyk_float_str(double x)
{
char buf[64];
char digits[32];
char *out = malloc(64);
double a;
int negative, p, at, i, n, decpt, k;
const char *e;
if (x != x) { strcpy(out, "nan"); return out; }
if (x > DBL_MAX) { strcpy(out, "inf"); return out; }
if (x < -DBL_MAX) { strcpy(out, "-inf"); return out; }
negative = x < 0.0 || (x == 0.0 && 1.0 / x < 0.0);
a = negative ? -x : x;
if (a == 0.0) { strcpy(out, negative ? "-0.0" : "0.0"); return out; }
for (p = 0; p < 16; p++) {
sprintf(buf, "%.*e", p, a);
if (strtod(buf, NULL) == a) break;
}
if (p == 16) sprintf(buf, "%.*e", 16, a);
e = strchr(buf, 'e');
at = (int)(e - buf);
n = 0;
for (i = 0; i < at; i++) if (buf[i] != '.') digits[n++] = buf[i];
while (n > 1 && digits[n - 1] == '0') n--;
digits[n] = '\0';
decpt = atoi(e + 1) + 1;
k = 0;
if (negative) out[k++] = '-';
if (decpt <= -4 || decpt > 16) {
out[k++] = digits[0];
if (n > 1) { out[k++] = '.'; strcpy(out + k, digits + 1); k += n - 1; }
sprintf(out + k, "e%d", decpt - 1);
return out;
}
if (decpt <= 0) {
out[k++] = '0';
out[k++] = '.';
for (i = 0; i < -decpt; i++) out[k++] = '0';
strcpy(out + k, digits);
return out;
}
if (decpt >= n) {
strcpy(out + k, digits);
k += n;
for (i = 0; i < decpt - n; i++) out[k++] = '0';
strcpy(out + k, ".0");
return out;
}
for (i = 0; i < decpt; i++) out[k++] = digits[i];
out[k++] = '.';
strcpy(out + k, digits + decpt);
return out;
}
// What the top level binds, where every function can read it; the top level below is what fills each of them in.
static int kick;
static int snare;
static int canvas;
static int presses;
// Declared ahead of their definitions: a Pyking function may be called before it is written.
static void play_kick(void);
static void play_sound(void);
static void play_kick(void)
{
double tmp1;
double tmp2;
(tmp1 = tinyweb_play__default__at(), tmp2 = tinyweb_play__default__volume(), tinyweb_play(kick, tmp1, tmp2, tinyweb_play__default__rate()));
}
static void play_sound(void)
{
double tmp3;
presses = presses + 1;
printf("%s\n", pyk_concat("snare, press ", pyk_int_str(presses)));
(tmp3 = tinyweb_play__default__at(), tinyweb_play(snare, tmp3, 0.5, tinyweb_play__default__rate()));
}
int main(void)
{
tinyweb_init();
tinyweb_web_init(0);
kick = tinyweb_load_sample("kick1.wav");
snare = tinyweb_load_sample("snare1.wav");
canvas = tinyweb_canvas(400, 200);
tinyweb_circle(canvas, 100, 50, 30, "#ff0000");
tinyweb_circle(canvas, 100, 50, 20, tinyweb_circle__default__color());
presses = 0;
tinyweb_button("Play kick", play_kick);
tinyweb_button("Play snare", play_sound);
tinyweb_button("Does nothing", tinyweb_button__default__on_click());
printf("%s\n", "built the page");
return 0;
}

lib_tinyweb_style

# The page's look: a theme, the style of every element of a kind, and
# the style of one element. The .events file presses the button, which
# changes the theme and one label while the page is showing.
import tinyweb as tw
tw.web_init(0)
tw.theme("paper")
tw.theme("no such theme")
tw.page_style(background = "#fdf6e3", color = "#586e75", font = "serif", size = 18, padding = 12, gap = 16)
tw.button_style(background = "#268bd2", color = "white", border = "none", radius = 14, font = "monospace", size = 16, padding = 8)
tw.label_style(background = "lightyellow", color = "#333333", border = "1px solid gold", radius = 4, font = "sans-serif", size = 14, padding = 6)
tw.slider_style(accent = "crimson", width = 240, color = "crimson", font = "monospace", size = 20)
tw.canvas_style(border = "3px solid teal", radius = 10)
title = tw.label("Styled")
tw.set_style(title, background = "navy", color = "white", border = "2px dashed orange", radius = 0, font = "Georgia", size = 28, padding = 10, width = 300)
level = tw.slider(0, 10, 4)
tw.set_colors(level, color = "seagreen")
c = tw.canvas(120, 60)
tw.set_style(c, border = "1px solid black")
def pressed():
tw.theme("retro")
tw.set_colors(title, background = "black")
print("now retro")
ok = tw.button("Retro", on_click = pressed)
tw.set_colors(ok, color = "black", background = "gold")

lib_tinyweb_style.zip

program.c, as Pyking generates it
// Generated from lib_tinyweb_style.pyk by the Pyking compiler. Do not edit.
// Target: c23. One translation unit of the program; compile it with the others.
#include <stdio.h>
#include "tinyweb.h"
// What the external modules this one imports implement.
void tinyweb_web_init(int);
int tinyweb_canvas(int, int);
int tinyweb_label(const char *);
// What the top level binds, where every function can read it; the top level below is what fills each of them in.
static int title;
static int level;
static int c;
static int ok;
// Declared ahead of their definitions: a Pyking function may be called before it is written.
static void pressed(void);
static void pressed(void)
{
tinyweb_theme("retro");
tinyweb_set_colors(title, tinyweb_set_colors__default__color(), "black");
printf("%s\n", "now retro");
}
int main(void)
{
const char *tmp1;
const char *tmp2;
int tmp3;
const char *tmp4;
int tmp5;
int tmp6;
const char *tmp7;
tinyweb_init();
tinyweb_web_init(0);
tinyweb_theme("paper");
tinyweb_theme("no such theme");
tinyweb_page_style("#fdf6e3", "#586e75", "serif", 18, 12, 16);
tinyweb_button_style("#268bd2", "white", "none", 14, "monospace", 16, 8);
tinyweb_label_style("lightyellow", "#333333", "1px solid gold", 4, "sans-serif", 14, 6);
tinyweb_slider_style("crimson", 240, "crimson", "monospace", 20);
tinyweb_canvas_style("3px solid teal", 10);
title = tinyweb_label("Styled");
tinyweb_set_style(title, "navy", "white", "2px dashed orange", 0, "Georgia", 28, 10, tinyweb_set_style__default__accent(), 300);
level = tinyweb_slider(0, 10, 4, tinyweb_slider__default__on_change());
tinyweb_set_colors(level, "seagreen", tinyweb_set_colors__default__background());
c = tinyweb_canvas(120, 60);
(tmp1 = tinyweb_set_style__default__background(), tmp2 = tinyweb_set_style__default__color(), tmp3 = tinyweb_set_style__default__radius(), tmp4 = tinyweb_set_style__default__font(), tmp5 = tinyweb_set_style__default__size(), tmp6 = tinyweb_set_style__default__padding(), tmp7 = tinyweb_set_style__default__accent(), tinyweb_set_style(c, tmp1, tmp2, "1px solid black", tmp3, tmp4, tmp5, tmp6, tmp7, tinyweb_set_style__default__width()));
ok = tinyweb_button("Retro", pressed);
tinyweb_set_colors(ok, "black", "gold");
return 0;
}