1. andrew cooke
  2. c-orm

Wiki

Clone wiki

c-orm / Philosophy

Architecture and Coding Conventions

Architecture

C-ORM is an exploratory, layered, open, low-impact library. By that, I mean:

  • It explores various ideas about how to best code in C. These include, for example, flow macros (see below) and using structs to namespace library functions. I think it's fashionable to use "opinionated" to describe libraries that take a consistent, although perhaps unorthodox, approach to problems, but I don't like that term because it implies that the approach is correct. It may be - is even likely - that some ideas here are not so great. If so, I will learn and try not to make the same mistake again...

  • At a low level, C-ORM will let you send a command (as a string) to the database and then execute a callback to return the results. At a slightly higher level it supports the proper escaping of parameters. Higher still is a "fluent interface" to common SQL queries. And on top of that, auto-generated sugar to handle structs. As we move upwards, the library is easier to use, but more restricted - the usual compromise with APIs. Exposing multiple layers lets you select the correct tool for the task at hand, so you only compromise where you can.

  • Unlike many "best practice" libraries (eg. C Interfaces and Implementations), C-ORM avoids using opaque data structures (ADTs). Instead, the approach here is a lot more like Python code - you can see all the details. You can even play around and modify things at a low level. Taken with the layered approch this helps "make hard things possible" (the main design should, I hope, "make easy things easy").

  • Where possible, the library uses and exposes basic types. So, for example, although it provides its own string library, most API calls take char *. And the string implementation itself exposes the underlying char * data for easy access. In other words, C-ORM tries to stay in the background. It should not pollute your entire codebase with its own types.

I hope you can see how these ideas are inter-related. The low API impact is possible because the data structures are open, for example.

Coding Conventions

Status Handling

Most functions (the exceptions are fluent interfaces; see below) return an integer status value. This is 0 (false) on success.

Functions that are typically called during resource recovery (at the end of a function, possibly on error) take the current status as an argument and return the pessimistic combination of that and their own status.

These two conventions, "simple enough" code, and NULL initial values, allow us to structure functions with a single exit point for cleanup:

    int myfunction(const char *argument, struct foo **retval) {
      int status = 0;
      struct bar *heap = NULL; 
      if (!(status = alloc_bar(&heap))) goto exit;
      // more calculations and checks here
    exit:
      if (heap) status = free_bar(heap, status);
      return status;
    }

Standard Flow

The pattern above is so common that it is abstracted into a set of macros, defined in isti_flow.h. The code above becomes:

    int myfunction(const char *argument, struct foo **retval) {
      STATUS;
      struct bar *heap = NULL; 
      CHECK(alloc_bar(&heap));
      // more calculations and checks here
      EXIT;
      if (heap) status = free_bar(heap, status);
      RETURN;
    }

This has two advantages. First, it removes some clutter (eg CHECK above). Second, it formalizes the approach, making it easier to spot (and correct) exceptions.

Fluent Interfaces

In some parts of C-ORM the API is designed to allow chained calls in a style commonly called a "fluent interface". For example:

    typedef struct foo {
      int id;
      int bar;
      char *baz;
    } foo;

    corm_foo_select *s = NULL;
    foo *f = NULL;
    CHECK(corm_foo.select(&s, db));
    CHECK(s->bar(s, "=", 42)->baz(s, "=", "towel")->_go_one(s, &f));

Here the bar and baz calls are accumulating information for the SQL select command. These calls return a pointer to an instance of corm_foo_select, which is also the first argument (following the standard "self" idiom in C). Since they cannot return status it is set on the chained structure (ie corm_foo_select.status). Each routine must then check this value on entry and set it on exit.

Namespaces

Functions are named by package, starting with the isti_ prefix. So string functions are prefixed with isti_str_, etc. This can get quite verbose (particularly with auto-generated code), so packages provide an alternative mechanism which resembles namespaces in other languages:

    ISTI_STR_AS(str)

    isti_str *s = NULL;
    str.alloc(&s);  // equivalent to isti_str_alloc()

Here str appears as as a prefix to the short function name alloc().

This is implemented as a local (static) struct, so is restricted to functions and, occasionally, other values (not types - hence the need for isti_str above).

Function Names

C-ORM tries to follow stdlib conventions, so, using the string API as an example:

    ISTI_STR_AS(str)
    isti_str *s;

    // as stdlib
    str.append(s, "some text");
    str.appendf(s, "%s %d", "foo", 42);  // varargs with format
    str.vappendf(s, "%s %d", ap);  // va_list equivalent

    // new
    str.concatn(s, "one", "two", "three", NULL);  // NULL terminated list
    str.vconcatn(s, ap);  // va_list equivalent

In addition:

  • An underscore (_) prefix is used in fluent interfaces to separate "operations" from SQL field names (see example above with bar() - a field name - and _go_one() - an operations).

  • An underscore (_) suffix is used to distinguish between similar functions that take different argument types. For example, a fluent SQL interface may use bar() to restrict a select with a single value (eg equality), and bar_() for a set of values (eg "in").

  • All-caps are used for strings (table and column names).

Updated