Some time has past (three years!) since I last wrote about API specifically about coroutines style APIs so I thought why not write another one about a different API type I encounter relatively often. The builder API.
Now first let me take a step back and put this into 20,000 feet view on where builder APIs are located in the grant scheme. In general everything in computing is separated into input, processing and finally output. In its most basic form I am currently typing on my keyboard. All pressed keys are processed from the OS up to the browser I am writing this in and finally rendered and displayed on the screen as output. Of course this example is very user centric view but still applys to all functionallity in need of an API as well. Another maybe more appplicable example is a collision API that takes in a number of collision primitives and their transformations, finds collisions between these primitives and outputs these collisions with additional information.
My previous post about coroutine style APIs showed a specific practical example on how a processing API could be written with focus on granularity and resource management. Ranging from diagonal API that handles all system resource management like memory or file access to orthogonal coroutine APIs with full API user control of resources including control over if the the functionality should further process at all.
Builder APIs in contrast are placed on the data conversion/packing end of things. They take in a specific input format, process it and finally output the same data in a different format.
Lets look at a relativ well know example of an API taking in scaled vector drawing commands and convert it directly into vertex, element and draw commands for rendering inside a GPU. This kind of API is for example is used inside nuklear to convert an internal primitive buffer (rects, lines, text,...) into an graphics API accessable vertex format:
enum svg_vtx_lay_attr {
SVG_VTX_POS,
// ...
};
enum svg_vtx_lay_fmt {
SVG_FMT_SCHAR,
SVG_FMT_R8G8B8,
// ...
};
enum svg_stroke {
SVG_STROKE_OPEN,
SVG_STROKE_CLOSED
};
struct svg_vtx_lay_elm {
enum svg_vtx_lay_attr attr;
enum svg_vtx_lay_fmt fmt;
int off;
};
struct svg_cmd {
unsigned int elem_cnt;
float clip[4];
handle tex;
};
struct svg_param {
const struct svg_vtx_lay_elm *layout;
int size;
int align;
};
struct svg_info {
int vtx_cnt;
int elm_cnt;
int cmd_cnt;
};
struct svg {
int error;
// ...
};
// init
void svg_begin(struct svg *b, const struct svg_param *param, struct storage *cmds, struct storage *vtx, struct storage *elm);
void svg_end(struct svg *b, struct svg_info *info);
// state
void svg_color(struct svg*, unsigned col);
void svg_rounding(struct svg*, float rounding);
void svg_line_thickness(struct svg*, float thickness);
// path
void svg_path_clear(struct svg*);
void svg_path_line_to(struct svg*, float x, float y);
void svg_path_arc_to(struct svg*, float x, float y, float radius, float a_min, float a_max, unsigned int num_seg);
void svg_path_rect_to(struct svg*, float ax, float ay, float bx, float by);
void svg_path_curve_to(struct svg*, const float *p2, const float *p3, const float *p4, unsigned int num_seg);
void svg_path_fill(struct svg*);
void svg_path_stroke(struct svg*, enum svg_stroke closed);
// stroke
void svg_stroke_line(struct svg*, float ax, float ay, float bx, float by);
void svg_stroke_rect(struct svg*, float x, float y, float w, float h, unsiged col, float rounding);
void svg_stroke_triangle(struct svg*, const float *a, const float *b, const float *c);
void svg_stroke_circle(struct svg*, float x, float y, float radius, unsigned int segs);
void svg_stroke_curve(struct svg*, const float *p0, const float *cp0, const float *cp1, const float *p1, unsigned int seg);
void svg_stroke_poly_line(struct svg*, const float *pnts, unsigned cnt, enum svg_draw_list_stroke);
// fill
void svg_fill_rect(struct svg*, float x, float y, float w, float h);
void svg_fill_triangle(struct svg*, const float *a, const float *b, const float *c);
void svg_fill_circle(struct svg*, float x, float y, float radius, unsigned segs);
void svg_fill_convex(struct svg*, const float *pnts, unsigned cnt);Most functions and structs should be relativ straightforward to understand. A number of structs to specify a custom output vertex layout with elements like position, texture coordinates and color and their type. A number of functions for path drawing and finally a number of functions for drawing primitives. So lets look at the usage code:
// specify vertex layout
struct your_vertex {
float pos[2];
float uv[2];
unsigned char color[4];
};
static const struct svg_vtx_lay_elm vtx_lay[] = {
{SVG_VTX_POS, SVG_FMT_FLT, offsetof(struct your_vertex, pos)},
{SVG_VTX_UV, SVG_FMT_FLT, offsetof(struct your_vertex, uv)},
{SVG_VTX_COL, SVG_FMT_R8G8B8A8, offsetof(struct your_vertex, color)},
{SVG_VTX_LAY_END}
};
struct svg_param cfg = {0};
cfg.size = sizeof(struct your_vertex);
cfg.align = alignof(struct your_vertex);
cfg.layout = vtx_lay;
// setup output memory
struct storage buf[3];
storage_init_fixed(&buf[0], calloc(2*1024*1024), 2*1024*1024);
storage_init_fixed(&buf[1], calloc(2*1024*1024), 2*1024*1024);
storage_init_fixed(&buf[2], calloc(2*1024*1024), 2*1024*1024);
// draw stuff
struct svg svg = {0};
svg_begin(&svg, &cfg, &buf[0], &buf[1], &buf[2]);
{
// ...
svg_color(&svg, svg_red);
svg_fill_rect(&svg, 50, 50, 120, 30);
// ...
}
svg_end(&svg, 0);
// draw to screen
int offset = 0;
svg_draw_foreach(cmd, buf[0]) {
if (!cmd->elem_cnt) {
continue;
}
glBindTexture(GL_TEXTURE_2D, (GLuint)cmd->texture.id);
glScissor((GLint)(cmd->clip[0]),
(GLint)((win_height - (GLint)(cmd->clip[1] + cmd->clip[3]))),
(GLint)(cmd->clip[2]), (GLint)(cmd->clip[3]);
glDrawElements(GL_TRIANGLES, (GLsizei)cmd->elem_count, GL_UNSIGNED_SHORT, offset);
offset += cmd->elem_count;
}First a vertex layout is specified and memory is reserved until finally we can specify primitives to be drawn. Finally the generated output is pushed to be rendered and output on the screen via graphics API. Specifically in this example I will skip resource handling for now and instead try to focus on the basic data flow for builder APIs. The reason for using a svg type renderer as the first example is that in this case no temporary format and resources are required instead primitives are directly converted into vertexes, elements and draw commands further simplifying the API.
Builder are the prototypical immediate mode APIs going so far as not keeping state between each run of
svg_begin to svg_end and the having the caller only directly push new state and therefore never mutates
any buffer state.
static const char *tbl_titles[] = {"Name", "Path", "Type",
"Size", "Permission", "Date Modified"};
static float tbl_panes[] = {-180.f, -6.0f, -225.0f, -6.0f, -50.0f, -6.0f,
-60.0f, -6.0f, -80.0f, -6.0f, 1.0f, -6.0f};
static const struct gui_lay_con tbl_con[cntof(tbl_panes)] = {
{.min = 100, .max = 400}, {.min = 6, .max = 6},
{.min = 150, .max = 400}, {.min = 6, .max = 6},
{.min = 50, .max = 400}, {.min = 6, .max = 6},
{.min = 50, .max = 400}, {.min = 6, .max = 6},
{.min = 50, .max = 400}, {.min = 6, .max = 6},
{.min = 100, .max = 400}};
static int tbl_sep[cntof(tbl_panes)];
int tbl_cols[cntof(tbl_panes)];
gui_tbl_hdr(ctx, &tbl, tbl_titles, tbl_cols, tbl_panes, tbl_sep, tbl_con,
cntof(tbl_panes));enum gui_tbl_hdr_builder_col_type {
GUI_TBL_HDR_COL_FIX,
GUI_TBL_HDR_COL_DYN,
};
struct gui_tbl_hdr_builder_col {
const char *title;
enum gui_tbl_hdr_builder_col_type type;
float size;
struct gui_lay_con con;
};
struct gui_tbl_hdr_builder {
int err;
int out_size;
int col_cnt;
// internal
struct gui_tbl_hdr_builder_col *cols;
int buf_cnt;
};
struct gui_tbl_hdr {
int col_cnt;
const char **titles;
struct gui_lay_con *cons;
float *panes;
int *state;
};
void gui_tbl_hdr_builder_begin(struct gui_tbl_builder *b, struct arena *a, int col_cnt);
void gui_tbl_hdr_builder_begin_fix(struct gui_tbl_builder *b, struct gui_tbl_hdr_builder_col *buf, int cnt);
void gui_tbl_hdr_builder_add_fix(struct gui_tbl_builder *b, const char *title, int size, struct gui_lay_con con);
void gui_tbl_hdr_builder_add_dyn(struct gui_tbl_builder *b, const char *title, struct gui_lay_con con);
void gui_tbl_hdr_builder_add(struct gui_tbl_builder *b, const struct gui_tbl_hdr_builder_col *cols, int cnt);
int gui_tbl_hdr_builder_end_inplace(struct gui_tbl_hdr **res, struct gui_tbl_builder *b, void *memory);
int gui_tbl_hdr_builder_end(struct gui_tbl_hdr **res, struct gui_tbl_builder *b, struct arena *a);enum gui_tbl_col_flags {
GUI_TBL_COL_TYPE = (1 << 0),
GUI_TBL_COL_SIZE = (1 << 1),
GUI_TBL_COL_PERMISSION = (1 << 2),
GUI_TBL_COL_DATE = (1 << 3),
GUI_TBL_COL_ALL = 0xffffffffu
};
static const struct gui_tbl_hdr_builder_col default_columns[] = {
{ .title = "Name", .type = GUI_TBL_HDR_COL_FIX, .size = 180, .con = { .min = 100, .max = 400 } },
{ .title = "Path", .type = GUI_TBL_HDR_COL_DYN, .size = 1.0f, .con = { .min = 150, .max = 400 } }
};
static const struct gui_tbl_hdr_builder_col optional_columns[] = {
{ .title = "Type", .type = GUI_TBL_HDR_COL_FIX, .size = 50, .con = { .min = 50, .max = 400 } },
{ .title = "Size", .type = GUI_TBL_HDR_COL_FIX, .size = 60, .con = { .min = 50, .max = 400 } }
{ .title = "Permission", .type = GUI_TBL_HDR_COL_FIX, .size = 80, .con = { .min = 50, .max = 400 } }
{ .title = "Date Modified", .type = GUI_TBL_HDR_COL_FIX, .size = 100, .con = { .min = 150, .max = 400 } }
};
#define MAX_COLS 32
struct gui_tbl_hdr_builder_col cols[MAX_COLS];
static unsigned col_flags = GUI_TBL_COL_ALL;
struct gui_tbl_hdr *hdr = 0;
struct gui_tbl_builder b = {0};
gui_tbl_hdr_builder_begin_fix(&b, cols, MAX_COLS);
{
gui_tbl_hdr_builder_add(&b, default_columns, cntof(default_columns));
for (int i = 0; i < cntof(optional_columns); ++i) {
if (col_flags & (1 << i)) {
gui_tbl_hdr_builder_add(b, &optional_columns[i], 1);
}
}
}
gui_tbl_hdr_builder_end(&hdr, &b, calloc(1, b.out_size));gui_tbl_hdr(ctx, &tbl, hdr);