The plugin system

The Machinery is built around a plugin model. All features, even the built-in ones, are provided through plugins. You can extend The Machinery by writing your own plugins.

When The Machinery launches, it loads all the plugins named tm_*.dll in its plugins/ folder. If you write your own plugins, name them so that they start with tm_ and put them in this folder, they will be loaded together with the built-in plugins.

Table of Content

About API's

The Machinery is organized into individual APIs that can be called to perform specific tasks. A plugin is a DLL that exposes one or several of these APIs. In order to implement its functionality, the plugin may in turn rely on APIs exposed by other plugins.

A central object called the API registry is used to keep track of all the APIs. When you want to use an API from another plugin, you ask the API registry for it. Similarly, you expose your APIs to the world by registering them with the API registry.

This may seem a bit abstract at this point, so let’s look at a concrete example, unicode.h which exposes an API for encoding and decoding Unicode strings:

#pragma once

#include "api_types.h"

struct tm_temp_allocator_i;

// API for converting between UTF-8 encoded text and UTF-16 or UTF-32. All strings in The Machinery
// are UTF-8 encoded, but UTF-16 and UTF-32 are sometimes needed to communicate with external APIs.
// For example, Windows uses UTF-16.
//
// !!! TODO: API-REVIEW
//     * Add `tm_str_t codepoint_range(tm_str_t s)`.

struct tm_unicode_api
{
    // UTF-8

    // Returns *true* if `utf8` is a valid UTF-8 string, *false* otherwise.
    bool (*is_valid)(const char *utf8);

    // Fixes the truncation of a UTF-8 encoded string `utf-8` by replacing any split codepoints at
    // the end of the string with `\0` bytes. You can use this after truncating a string to make
    // sure that the resulting string is still a valid UTF-8 string.
    void (*truncate)(char *utf8);

    // UTF-32

    // Encodes the `codepoint` as UTF-8 into `utf8` and returns a pointer to the position where
    // to insert the next codepoint. `utf8` should have room for at least four bytes (the
    // maximum size of a UTF-8 encoded codepoint).
    char *(*utf8_encode)(char *utf8, uint32_t codepoint);

    // Decodes and returns the first codepoint in the UTF-8 string `utf8`. The string pointer is
    // advanced to point to the next codepoint in the string. Will generate an error message
    // if the string is not a UTF-8 string.
    uint32_t (*utf8_decode)(const char **utf8);

    // Returns the number of codepoints in `utf8`.
    uint32_t (*utf8_num_codepoints)(const char *utf8);

    // Decodes the first `n` codepoints in `utf8` to the `codepoints` buffer. If `utf8`
    // contains fewer than `n` codepoints -- decodes as many codepoints there are in `utf8`.
    // Returns the number of decoded codepoints.
    uint32_t (*utf8_decode_n)(uint32_t *codepoints, uint32_t n, const char *utf8);

    // Converts a UTF-8 encoded string to a UTF-32 encoded one, allocated with the supplied
    // temp allocator. Will generate an error message if the string is not a UTF-8 string.
    uint32_t *(*utf8_to_utf32)(const char *utf8, struct tm_temp_allocator_i *ta);

    // As [[utf8_to_utf32()]], but uses an explicit length instead of a zero terminated string. Note
    // that the result string will still be zero terminated.
    uint32_t *(*utf8_to_utf32_n)(const char *utf8, uint32_t n, struct tm_temp_allocator_i *ta);

    // Converts a UTF-32 encoded string to a UTF-8 encoded one, allocated with the specified temp
    // allocator. Generates an error if the data is outside the UTF-8 encoding range.
    char *(*utf32_to_utf8)(const uint32_t *utf32, struct tm_temp_allocator_i *ta);

    // As [[utf32_to_utf8()]], but uses an explicit length instead of a zero terminated string. Note
    // that the result string will still be zero terminated.
    char *(*utf32_to_utf8_n)(const uint32_t *utf32, uint32_t n, struct tm_temp_allocator_i *ta);

    // UTF-16

    // Encodes the codepoint as UTF-16 into `utf16` and returns a pointer to the position where to
    // insert the next codepoint. `utf16` should have at room for at least two `uint16_t` (the
    // maximum size of a UTF-16 encoded codepoint).
    uint16_t *(*utf16_encode)(uint16_t *utf16, uint32_t codepoint);

    // Decodes and returns the first codepoint in the UTF-16 string `utf16`. The string pointer is
    // advanced to point to the next codepoint in the string.
    uint32_t (*utf16_decode)(const uint16_t **utf16);

    // Converts a UTF-8 encoded string to a UTF-16 encoded one, allocated with the supplied temp
    // allocator. Will generate an error message if the data is outside the UTF-8 encoding range.
    uint16_t *(*utf8_to_utf16)(const char *utf8, struct tm_temp_allocator_i *ta);

    // As [[utf8_to_utf16()]] but uses an explicit length instead of a zero terminated string. Note
    // that the result string will still be zero terminated.
    uint16_t *(*utf8_to_utf16_n)(const char *utf8, uint32_t n, struct tm_temp_allocator_i *ta);

    // Converts a UTF-16 encoded string to a UTF-8 encoded one, allocated with the specified
    // temp allocator. Will generate an error message if the string is not a UTF-16 string.
    char *(*utf16_to_utf8)(const uint16_t *utf16, struct tm_temp_allocator_i *ta);

    // As [[utf16_to_utf8()]] but uses an explicit length instead of a zero terminated string. Note
    // that the result string will still be zero terminated.
    char *(*utf16_to_utf8_n)(const uint16_t *utf16, uint32_t n, struct tm_temp_allocator_i *ta);
};

#define tm_unicode_api_version TM_VERSION(1, 0, 0)

// Returns a UTF-8 string representing the codepoint `cp`. The string is stack allocated.
#define tm_codepoint_to_utf8(cp) tm_codepoint_to_utf8_internal(cp, (char[5]){ 0 })

#if defined(TM_LINKS_FOUNDATION)
extern struct tm_unicode_api *tm_unicode_api;
#endif

Let’s go through this.

First, the code includes <api_types.h>. This is a shared header with common type declarations, it includes things like <stdbool.h> and <stdint.h> and also defines a few The Machinery specific types, such as tm_vec3_t.

In The Machinery we have a rule that header files can't include other header files (except for <api_types.h>). This helps keep compile times down, but it also simplifies the structure of the code. When you read a header file you don’t have to follow a long chain of other header files to understand what is happening.

Next follows a block of forward struct declarations (in this case only one).

Next, we have the name of this API defined as a constant tm_unicode_api, followed by the struct tm_unicode_api that defines the functions in the API.

To use this API, you would first use the API registry to query for the API pointer, then using that pointer, call the functions of the API:

static struct tm_unicode_api *tm_unicode_api;
#include <foundation/api_registry.h>
#include <foundation/unicode.h>

static void demo_usage(char *utf8, uint32_t codepoint)
{
    tm_unicode_api->utf8_encode(utf8, codepoint);
    //more code...
}

TM_DLL_EXPORT void tm_load_plugin(struct tm_api_registry_api *reg, bool load)
{
    tm_unicode_api = tm_get_api(reg, tm_unicode_api);
}

The different APIs that you can query for and use are documented in their respective header files, and in the apidoc.md.html documentation file (which is just extracted from the headers). Consult these files for information on how to use the various APIs that are available in The Machinery.

In addition to APIs defined in header files, The Machinery also contains some header files with inline functions that you can include directly into your implementation files. For example <math.inl> provides common mathematical operations on vectors and matrices, while <carray.inl> provides a “stretchy-buffer” implementation (i.e. a C version of C++’s std::vector).

About Interfaces

We also add an implementation of the unit test interface to the registry. The API registry has support for both APIs and interfaces. The difference is that APIs only have a single implementation, whereas interfaces can have many implementations. For example, all code that can be unit-tested implements the unit test interface. Unit test programs can query the API registry to find all these implementations and run all the unit tests.

To extend the editor you add implementations to the interfaces used by the editor. For example, you can add implementations of the tm_the_truth_create_types_i in order to create new data types in The Truth, and add implementations of the tm_entity_create_component_i in order to define new entity components. See the sample plugin examples.

It does not matter in which order the plugins are loaded. If you query for a plugin that hasn’t yet been registered, you get a pointer to a nulled struct back. When the plugin is loaded, that struct is filled in with the actual function pointers. As long as you don’t call the functions before the plugin that implements them has been loaded, you are good. (You can test this by checking for NULL pointers in the struct.)