How ArmorPaint Dynamically Loads Extensions Like io_gltf, io_fbx, and io_exr via plugins/plugins.c

ArmorPaint's plugin architecture uses registration tables and hash maps in plugins/plugins.c to map file extensions to C function pointers, enabling runtime dispatch of parsers like io_gltf_parse, io_fbx_parse, and io_exr_parse without hardcoded conditional logic.

ArmorPaint is an open-source 3D texture painting application developed by armory3d. Understanding how the ArmorPaint plugin system dynamically loads extensions at runtime reveals a sophisticated data-driven dispatch mechanism that decouples file format support from the core engine using static compilation combined with runtime hash map lookups.

The Runtime Loading Architecture

The dynamic loading process centers on plugins_init() inside paint/plugins/plugins.c. This function executes during application startup to establish a lookup table mapping file extensions to their corresponding parser implementations.

Initializing Format Lists

First, the system initializes dynamic string arrays that track supported extensions. The initialization calls path_texture_formats() and path_mesh_formats() to create these arrays, which the UI queries to display available import options in file dialogs.

path_texture_formats();
path_mesh_formats();

Registering Parser Functions

Next, plugins_init() populates two global hash maps with function pointers: import_texture_importers for image formats and import_mesh_importers for geometry. The maps use file extensions as keys (e.g., "exr", "fbx") and store pointers to the parser functions as values.

For the EXR texture and FBX mesh loaders, the registration looks like this:

any_map_set(import_texture_importers, "exr", import_exr);
any_map_set(import_mesh_importers, "fbx", import_fbx);

The actual parser implementations reside in dedicated plugin directories. For example, import_exr lives in paint/plugins/io_exr/io_exr.c, while io_fbx_parse is defined in paint/plugins/io_fbx/io_fbx.c. The GLTF parsers follow the same pattern in their respective source files.

Populating Extension Arrays

Finally, the system pushes the extension strings onto public format arrays. This step ensures the UI can enumerate supported types without accessing internal hash maps directly:

any_array_push(_path_texture_formats, "exr");
any_array_push(_path_mesh_formats, "fbx");

Runtime Dispatch Mechanism

When a user opens a file, the core engine extracts the extension and queries the appropriate importer map. This data-driven dispatch eliminates hardcoded switch statements and conditional chains.

char *ext = string_last_index_of(filename, '.') + 1;

if (any_map_has(import_mesh_importers, ext)) {
    void *(*loader)(char *) = any_map_get(import_mesh_importers, ext);
    void *mesh = loader(filename);
    // mesh now holds the loaded geometry
}

If the extension exists in the map, the stored function pointer executes immediately. The engine remains agnostic about which concrete parser runs; it simply invokes the generic pointer retrieved via any_map_get().

Implementing a New Extension

Adding support for a new file format requires three straightforward modifications to the ArmorPaint dynamic plugin loading system:

  1. Implement the parser using the signature void *my_parser(char *path) in a new source file under paint/plugins/.
  2. Register the mapping in plugins_init() by calling any_map_set() to link the extension string to your function.
  3. Update the format arrays by pushing the extension onto _path_mesh_formats or _path_texture_formats so the UI recognizes the new type.

For example, to add Alembic (.abc) support:

/* Declare the parser in plugins.c */
void *import_abc(char *path);

/* Registration inside plugins_init() */
any_map_set(import_mesh_importers, "abc", import_abc);
any_array_push(_path_mesh_formats, "abc");

Compile-Time Configuration Flags

The plugin system respects two conditional compilation flags that control how ArmorPaint loads extensions at runtime:

  • WITH_PLUGINS – When undefined, the entire plugin initialization system is excluded from the build, creating a minimal binary without import/export capabilities.
  • WITH_EXTERNAL – Enables support for third-party shared libraries via external_init(). When set, plugins_init() invokes external_init() to load additional plugins compiled as separate shared objects (.so or .dll files) rather than statically linked code.
#ifdef WITH_EXTERNAL
void external_init();
#endif

void plugins_init() {
    // ... core registrations ...
    #ifdef WITH_EXTERNAL
    external_init();
    #endif
}

Summary

  • plugins_init() in paint/plugins/plugins.c serves as the central registration hub executed at application startup.
  • Hash maps (import_mesh_importers, import_texture_importers) store function pointers keyed by extension strings like "gltf", "fbx", and "exr".
  • Runtime dispatch occurs through any_map_get(), retrieving the appropriate parser without hardcoded conditional logic.
  • Parser implementations reside in dedicated directories such as io_fbx/io_fbx.c and io_exr/io_exr.c, maintaining separation between the core engine and format-specific code.
  • Compile-time flags (WITH_PLUGINS, WITH_EXTERNAL) optionally disable the system or enable external shared library loading for proprietary extensions.

Frequently Asked Questions

How does ArmorPaint determine which parser to use for a specific file extension?

ArmorPaint extracts the file extension from the path and queries the corresponding hash map using any_map_has(). If the extension exists in import_mesh_importers or import_texture_importers, the system retrieves the function pointer via any_map_get() and executes it immediately. This constant-time lookup eliminates the need for lengthy conditional chains in the file loading code.

Can I add support for a new file format without modifying the core engine source code?

Yes. You only need to implement a parser function matching the signature void *parser(char *path), add a registration line in plugins_init() inside paint/plugins/plugins.c, and push the extension string onto the appropriate format array. The core engine automatically recognizes the new format through the hash map dispatch system without requiring changes to its loading logic or UI code.

What is the difference between the WITH_PLUGINS and WITH_EXTERNAL compile flags?

WITH_PLUGINS controls whether the entire plugin registration system compiles into the binary. When undefined, plugins_init() and all importer registrations are excluded, creating a minimal build. WITH_EXTERNAL specifically enables loading third-party shared libraries at runtime through external_init(), allowing proprietary or optional plugins to exist as separate .so or .dll files rather than being statically linked into the main executable.

Where are the actual io_gltf, io_fbx, and io_exr parser functions defined?

The parser implementations reside in their respective plugin directories within paint/plugins/. For example, io_fbx_parse is implemented in paint/plugins/io_fbx/io_fbx.c, and io_exr_parse lives in paint/plugins/io_exr/io_exr.c. These functions are compiled into the main binary and registered at startup via function pointers, making the loading "dynamic" in terms of runtime dispatch rather than dynamic linking of shared libraries.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →