Proven C Book←↑→

74 Objects built in C — systems and papers

What to know first

chapter 72, Dynamic virtual tables · tables built at run time, selectors and caches
chapter 73, The cost of an indirect call · measurement, inline caches, binary branches
chapter 71, Building the table · derived tables that embed the base table, size fields
chapter 68, Functions as values · the bones of GObject

Looking back

In the last four chapters we built function-pointer tables, added inheritance, looked up tables at run time and measured what it costs. Did this book think of these things first?

A. No. Almost all of them were built decades ago by someone, in C or on top of C. Objective-C began as a preprocessor that attached messages to C; the X Window widgets used class records that initialise the parent first back in the 1980s; and SQLite has lasted nearly twenty years with a table that carries a version number. And there are papers that measured the cost. This chapter tours them — not how to use them, but what they thought.

The need for this chapter, and its context

Knowing what others built brings two things. One is names — if you can say “this is an inline cache” or “this is a versioned table”, you read others’ code faster, and a code review needs no long explanation. The other is a record of failures — knowing where a large system paid a price, you avoid the same mistake in advance.

By the end of this chapter

For each case we note three things: what was built, one idea worth noticing, and which section of this part it connects to. We start with the people who put a language on top of C (Cfront, Cox’s preprocessor, Schreiner, Vala) and look at Objective-C in a little more detail. Then the tables that went into real systems — Xt, GObject, CPython, kernels, COM, SQLite, OpenSSL — then the libraries that tried other ideas, and the papers that measured the cost of dispatch. At the end the ideas worth noticing are gathered in one table. Others’ code is not reproduced here — the sources are named and the ideas retold.

The questions this chapter answers

  1. Which is better — a translator that emits C, or an imitation written in C?

74.1 People who put a language on top of C#

Imitating objects in C, everyone thinks at least once: “if only a machine would write this repetition for me”. Some people pushed that thought all the way. The results split in two — those that stayed translators that emit C, and those that became a different language in the end.

Q. Which is better — a translator that emits C, or an imitation written in C?

A. A translator removes repetition, and puts a compiler check where people would slip. In exchange there is one more tool, error messages and the debugger point at translated C, and the whole team has to learn the language. Chapter 75′s warning “do not make a class-declaration language out of macros” is aimed at translators half built out of macros. If you mean to finish it, build a real translator like Cfront or Vala; if not, stay with an imitation that reads as C.

74.2 A little Objective-C#

Objective-C was refined at the company Cox and Tom Love founded (Productivity Products International, later Stepstone). In 1988 NeXT licensed it as the language of NEXTSTEP, and through NeXT it was used for decades in Apple’s macOS and iOS. Features such as categories and protocols were added in the NeXT years.4

Seen with the eyes of this part, Objective-C is C with one thing added. It takes C code as it is and puts Smalltalk’s message sending on top.

@interface Shape
- (double)area;                 /* a method declaration */
@end
@interface Circle : Shape       /* inheritance */
@end
@interface Circle (Describing)  /* a category: adds methods to an existing class */
- (void)describe;
@end

double total(Shape *s) { return [s area]; }   /* sending a message */

It is not much of an exaggeration to say that the last line’s [s area] is the whole language. The compiler turns that expression into a call to a run-time function. In Apple’s run-time it is objc_msgSend(s, @selector(area)) — given the receiver and the selector, that function finds the method and jumps to it. The way it finds it is chapter 72′s lookup.c exactly. It gets the class from the isa at the head of the object, looks in that class’s (selector → function) cache first, and if not there scans the method lists, climbs to the parent, puts what it found in the cache, and if nothing is found at all hands over to message forwarding.5

The same syntax becomes a different C call depending on the run-time. On this machine (x86-64 Linux), Clang 19.1 targets by default the GNU run-time that GCC uses, and emitted the total above in two steps: get the function with objc_msg_lookup(s, sel), then jump to it. With -fobjc-runtime=gnustep-2.0 or -fobjc-runtime=macosx-10.15 it emitted a single objc_msgSend. The difference is whether lookup and call are tied into one function or kept apart — either way, a message is one C function call.

This language left three things to C.

74.3 Class records of the 1980s — the X Toolkit Intrinsics#

The X Window System’s X Toolkit Intrinsics (Xt) is an object system for widgets built in C. Settled at the end of the 1980s, it already had almost every piece of this part.6

class-records

Figure 74.1 — Xt’s class records. A derived record starts with its parent record’s exact shape.

Parent first, then self — the order of “fill” among chapter 72′s five steps was written into a standard document by this system in the 1980s.

74.4 GObject — the largest model case#

GLib’s GObject is the largest and longest-used object system built in C. Countless GTK and GNOME programs stand on it, and the same objects are called from other languages such as Python, JavaScript and Rust. Chapter 68 showed its bones; here we look at what it got right and in what order it builds its tables.7

StructureHow GObject does it
State kept apart from the tableevery instance struct has a GTypeInstance as its first member, whose first bytes point to the class structure. The class structure (our table) exists once per kind
Inheriting the tablethe class structure is made once, when the first instance of the kind is created, and starts as a copy of the parent’s class structure. A derived kind overrides only the slots it changes
Inheriting the statethe first member of a derived instance is the parent instance
Constructionone g_object_new makes every kind. The reference count starts at 1
Lifetimeg_object_ref and g_object_unref. When the count reaches 0, dispose and then finalize
Castschecking cast macros such as VIEWER_FILE(obj) (a warning when wrong) and kind-check macros such as VIEWER_IS_FILE(obj)
BoilerplateG_DECLARE_FINAL_TYPE declares the cast and kind-check macros, and G_DEFINE_TYPE makes the …_get_type function and the parent-class pointer

Table 74.1 — The structure of GObject

What it got right matches this part’s principles.

The costs are plain too. Dozens of lines of boilerplate per kind, the cost of a run-time type system, and time to learn it. And GObject is for places that really need a large object system — hundreds of widget kinds, bindings to many languages, decades of compatibility. Copy GObject’s shape into a small library and you copy only its costs. What to learn from it is the principles.

74.4.1 The order in which the table is built#

GObject’s class structure is a dynamic table in the sense of chapter 72. The type system builds it when the type is first used. The order is this.8

  1. Build the parent’s class structure first (if that is also its first time).
  2. Copy the parent’s class structure into the front of the new class structure. Slots not overridden are therefore the parent’s.
  3. Call the base_init functions the ancestors registered, from the topmost ancestor down.
  4. Call this type’s class_init. Here the derived type overrides only the slots it changes, fills new slots, and registers properties and signals.

It is the same order as what chapter 72′s build_table.c did — build the parent first, copy, override your own slots. The difference is verification and sealing. The type system does not check for empty slots — whether to fill a slot is up to each class_init — and it does not make a finished class structure read-only.

74.5 Python’s type objects — CPython#

Every CPython object keeps a reference count and a type pointer at its head (PyObject’s ob_refcnt and ob_type). The PyTypeObject that type pointer points to is a table with dozens of slots whose names start with tp_ — hashing, comparison, conversion to a string, attribute lookup, calling. To build a new type in C, you fill only the slots that change, leave the rest NULL and call PyType_Ready. That function then inherits and fills the empty slots from the parent type (tp_base).9

This filling by PyType_Ready is one more answer to chapter 70′s “empty slots”. Instead of making every caller check for NULL slots, PyType_Ready fills them from the parent at the one place where the type is built — a bundle of defaults applied at run time.

74.6 Tables in kernels — Linux and FreeBSD#

The Linux kernel is one of the largest object systems built in C. Every file system, device and network protocol has tables of function pointers (file_operations, inode_operations, net_device_ops). In two articles for LWN in 2011, Neil Brown laid out its patterns — tables holding only function pointers as the norm, the table pointer in the object, and container_of to get back the outer object from an embedded base.10 Chapter 70′s “pure tables” and “empty slots” also lean on those articles.

FreeBSD’s kobj(9) took another road. A class hands out a list of (method label, function) pairs. When the class is first used, the kernel turns that list into a form that is quick to search — the manual calls this “compiling” the class. The manual says the dispatch after that is “only slightly more expensive than a direct function call” — their words, not something this book measured.11 It looks methods up by label, but does the preparation all at once the first time — a place between selector lookup and a static table.

74.7 COM from C#

Microsoft’s COM is a language-independent object convention. The only way to an object is through interfaces, and an interface is a table of function pointers. Seen from C, every COM object is a struct whose first member is a table pointer (lpVtbl), and the first three slots of every table are always QueryInterface, AddRef and Release.12

/* calling a COM object from C (the C flavour of the Windows headers) */
hr = obj->lpVtbl->QueryInterface(obj, &IID_IStream, (void **)&stream);
stream->lpVtbl->Release(stream);

Two things in this convention are worth noticing.

74.8 Tables that survive new versions — SQLite and OpenSSL#

SQLite routes everything it does with the operating system through a table called the VFS (sqlite3_vfs). The table’s first slot is a version number, iVersion, and the documentation says “new fields may be appended in future versions, and iVersion will increment whenever this happens”. iVersion became 1 in SQLite 3.5.0, 2 in 3.7.0 and 3 in 3.7.6. SQLite calls xCurrentTimeInt64, added in version 2, “only if iVersion is 2 or greater and the slot is not NULL” — it reads only what it knows and otherwise takes the old road.13

Written out from the caller’s side, it looks like this — not SQLite’s code, but the same rule rewritten for this book.

struct vfs {
    int version;                                           /* rises per new slot */
    int (*current_time)(struct vfs *, double *);           /* version 1: days */
    int (*current_time_ms)(struct vfs *, long long *);     /* version 2 */
};

long long now_ms(struct vfs *v)
{
    long long ms;
    if (v->version >= 2 && v->current_time_ms && v->current_time_ms(v, &ms) == 0)
        return ms;                             /* knows the new slot */
    double days = 0;
    v->current_time(v, &days);                 /* the old road */
    return (long long)(days * 86400000.0);
}

A version 1 table has no current_time_ms slot at all. So the version number is checked first, and the slot is read only in a version that has it.

The same documentation also records a time the rule was broken. It says that “due to an oversight” the struct changed in 3.6.0, yet iVersion did not go up. That means the version number alone cannot tell the struct’s shape before 3.6.0 from its shape after. A real program shows why chapter 71′s rule of writing down the size or a version number must be a rule — break it once and that number can never again be fully trusted.

OpenSSL 3 solved the version problem differently. A provider hands out its functions as an array of OSSL_DISPATCH — (function number, function pointer) pairs. The receiver skips numbers it does not know. So a provider built for one version of OpenSSL works with others too. There are no slot positions, so the rule “add only at the end” is not needed. The cost is a lookup — the array is scanned to match numbers before use. The array’s end marker was a pair of number 0 and NULL in the 3.0 documentation, and from the 3.3 documentation on is written with the OSSL_DISPATCH_END macro.14

74.9 Other ideas — libraries and experiments#

This part followed the shape “one table, one object”. Some attempts went outside it. We look at them not for whether they are usable but for what they tried.

Rows a, columns bcirclerectangle
circlehit_circle_circlehit_circle_rect
rectanglehit_rect_circlehit_rect_rect

Table 74.2 — A multi-method — choosing a function by the kinds of two arguments

With dispatch through one table per object, hit(a, b) chooses by a’s table alone, and b’s kind has to be sorted out again with a switch inside that function. A multi-method looks at both kinds at once and finds the cell.

74.10 Papers that measured dispatch#

Chapter 73′s measurement belongs to this one machine. There are papers that asked the same question earlier and more widely. Their numbers belong to their machines and their years, so they are not repeated; only what they asked is noted.

74.11 Ideas worth noticing in these systems, in one table#

IdeaWhere it appeared (this chapter)Its place in this part
Separate the table from the object, and make it constkernel tables, Xt’s class recordschapter 70′s three parts
Fill empty slots where the table is builtCPython’s PyType_Readychapter 70′s empty slots
A derived table that embeds the base tableXt’s part records, GObject’s class structureschapter 71′s inheritance
Parent first, then selfXt’s class_part_initialize, GObject’s class_initchapter 72′s five steps
Table and lifetime in one contractCOM’s IUnknownchapter 71′s lifetime
A table with a version numberSQLite’s iVersion (and 3.6.0, which broke it)chapter 71′s size and version fields
Slots matched by numberOpenSSL 3′s OSSL_DISPATCHchapter 72′s tables whose versions change
Asking for an interface separatelyCOM’s QueryInterfacechapter 72′s several interfaces
Look up by name, and cacheObjective-C’s objc_msgSendchapter 72′s selectors
Harden into a table the first timethe class compilation of FreeBSD’s kobjchapter 72′s itab
A cache at the call siteDeutsch and Schiffman; Hölzle and colleagueschapter 73′s inline caches
Dispatch without a tableZendra and colleagues’ SmallEiffelchapter 73′s binary branches
Choosing by several argumentsCOS’s multi-methods— (an idea outside this part)
The table is an object tooPiumarta and Warth— (an idea outside this part)

Table 74.3 — The ideas of this part, and where they first appeared

74.12 Summary#

TopicThe point
Languages on top of CCfront turned C++ into C, Cox’s preprocessor Smalltalk’s messages into C (it became Objective-C), Vala a C#-like syntax into GObject C. A machine writes the repetition — build it all the way, or not at all
Objective-CC with one thing added: sending messages. [s area] is a call to the run-time (objc_msgSend or objc_msg_lookup); the per-class cache cuts the cost of lookup
Xtclass records of the 1980s: superclass, part records, initialisation once and in a chain
GObjectconventions kept by macros and run-time checks. The class structure is a dynamic table that copies the parent and overrides
CPythonPyType_Ready fills NULL slots from the parent
KernelsLinux: pure tables and container_of. FreeBSD kobj: the method list turned into a quick-to-search form on first use
COMthe first slots of every table manage lifetime (reference counts). Instead of changing an interface, add another
SQLite and OpenSSLa versioned table and the record of breaking it; slots matched by number
Experiments and papersmulti-methods, the table as an object. Inline caches, PICs, the direct cost of virtual calls, dispatch without tables — the numbers are their machines’

Table 74.4 — Objects built in C — one line each

Having toured what others built, one question remains — when to stop. The last chapter measures the limits of macros, of principles and of object orientation itself, and closes this part by returning to the first idea.

Notes

  1. Computer History Museum, “C++ Historical Sources Archive” (softwarepreservation.computerhistory.org/c_plus_plus). ↩
  2. B. J. Cox, “The Object Oriented Pre-Compiler: Programming Smalltalk 80 Methods in C Language”, SIGPLAN Notices 18(1):15–22, 1983. ↩
  3. GNOME Wiki, “Projects/Vala/About”. ↩
  4. B. J. Cox, S. Naroff, H. Hsu, “The Origins of Objective-C at PPI/Stepstone and Its Evolution at NeXT”, HOPL IV, Proc. ACM Program. Lang. 4(HOPL), 2020. ↩
  5. Apple, “Objective-C Runtime Programming Guide”, “Messaging”. For the machine-code details of probing the cache, M. Ash, “Friday Q&A 2017-06-30: Dissecting objc_msgSend on ARM64”. ↩
  6. X Consortium, “X Toolkit Intrinsics — C Language Interface” (X11R7.7 edition), the chapter “Widget Classes”. ↩
  7. GObject documentation, “Type system concepts” (docs.gtk.org/gobject/concepts.html). ↩
  8. GObject documentation, “Type system concepts”, “Instantiatable classed types: objects”. ↩
  9. Python documentation, “Type Object Structures” (c-api/typeobj). A table gives the inheritance rule for each slot — it differs by version. ↩
  10. N. Brown, “Object-oriented design patterns in the kernel”, parts 1 and 2, LWN.net, 2011-06. ↩
  11. FreeBSD, kobj(9). The device driver framework newbus stands on it. ↩
  12. Microsoft Learn, “COM Objects and Interfaces”, “The IUnknown Interface”. ↩
  13. SQLite, “OS Interface Object” (c3ref/vfs.html). ↩
  14. OpenSSL documentation, OSSL_DISPATCH(3), 3.0 and 3.3 editions. ↩
  15. L. Deniau, “The C Object System: Using C as a High-Level Object-Oriented Language”, arXiv:1003.2547, 2010. ↩
  16. I. Piumarta, A. Warth, “Open, Extensible Object Models”, Self-Sustaining Systems (S3 2008), 2008. The line count was checked in the text of the edition VPRI published (objmodel2.pdf). ↩
  17. D. Holden, Cello, the introduction (libcello.org) and the repository’s README. ↩
  18. D. Holden, “A Fat Pointer Library” (libcello.org/learn). ↩
  19. K. Driesen, U. Hölzle, OOPSLA 1996, 306–323. ↩
  20. O. Zendra, D. Colnet, S. Collin, OOPSLA 1997, 125–141. ↩