Updated 7 November 2026

Core8Bit native plugin SDK

A native plugin is compiled C or C++ code that a Lua game loads and drives. Use a plugin when Lua is too slow: emulators, software renderers, large ports. The plugin draws into the framebuffer, plays audio and reads files through one function table, C8B_API (current version: 7).

Contents

Getting started

Building a plugin walks through a complete example: a minimal plugin, the console and emulator builds, and the main.lua that runs it.

How a plugin works

The same source code is built twice:

Target File How it is loaded
Console (STM32H743, Cortex-M7) .bin: a raw image linked at the fixed address 0xC1620000, starting with a 48-byte header (magic C8P1) The console copies the file into the plugin slot and calls the entry points listed in the header.
Emulator (macOS, Linux, Windows) .dylib, .so or .dll The emulator opens the shared library and looks up the exported functions by name.

Only one plugin can be loaded at a time.

Required exports

int         plugin_init(C8B_API* api);        /* return 0 on success; keep the api pointer */
bool        plugin_update(void);              /* once per frame; return false to stop */
void        plugin_cleanup(void);
int64_t     plugin_call(uint8_t func_id, const int64_t* args, uint8_t argc);  /* optional */
const char* plugin_get_exports(void);         /* optional, "name:argc,..." */

plugin_init, plugin_update and plugin_cleanup are required; a plugin without them is rejected. plugin_call and plugin_get_exports are optional: without them plugin.call returns -1 and plugin.getExports returns "".

plugin_init receives the C8B_API table. It holds version, the screen size (480 × 272) and the function pointers described in the API reference. Functions added after API v1 can be NULL on older firmware or on one platform, so check api->version and the pointer before calling them.

The SDK header c8b_plugin.h wraps these exports in macros (C8B_PLUGIN_GLOBALS, C8B_PLUGIN_MAIN, C8B_PLUGIN_UPDATE, C8B_PLUGIN_CLEANUP) and runs the console startup code for you.

Lifecycle

Lua What happens
plugin.load("plugins/x.bin") The file is loaded and its header checked. Loading a second plugin first cleans up the current one.
plugin.init() The plugin audio queue is emptied and plugin_init(api) runs. On the console the plugin must now zero .bss, copy its ITCM/DTCM code and data, set up its heap and run constructors; the SDK startup code does this.
plugin.call(0, ...) By convention, call 0 loads the content (game data, ROM).
plugin.update() every frame plugin_update() draws into get_framebuffer(). The console presents the frame after the Lua update function returns.
plugin.cleanup() plugin_cleanup() runs. The console then empties the audio queue, closes files left open, frees memory still held from alloc and resets the present and FastBuffer modes.

Returning false from plugin_update does not unload the plugin. Lua receives false and calls plugin.cleanup().

Loading a plugin from Lua

Pass the same .bin path on both platforms. The emulator replaces .bin with the host library extension (.dylib, .so or .dll) and looks for it in the game folder.

Lua Availability Description
plugin.load(path) → boolean Console and emulator A relative path is resolved against the game folder.
plugin.init() → integer Console and emulator 0 = success.
plugin.update() → boolean Console and emulator false = the plugin wants to stop.
plugin.cleanup() Console and emulator Unloads the plugin.
plugin.call(id, ...) → integer Console and emulator Up to 8 arguments: integers, numbers (truncated), booleans (0/1) and strings (a pointer valid only during the call). Without a ready plugin the console returns -1 and the emulator raises an error.
plugin.getExports() → string Console and emulator The string from plugin_get_exports.
plugin.saveState(), loadState(s), fullSaveState(), fullLoadState(s) Console and emulator See Save states.
plugin.isReady(), plugin.getState() Console only
plugin.isLoaded(), plugin.isInitialized(), plugin.loadPath(), plugin.setWorkingDir() Emulator only Check before calling: if plugin.isLoaded then ... end.

plugin.call ids belong to your plugin, except 20, 21, 22, 30, 31 and 32, which the save-state functions use.

local active = false

local function start()
    if not plugin.load("plugins/mygame_plugin.bin") then return end
    if plugin.init() ~= 0 then return end
    active = true
end

function c8b.update()
    if not active then start() return end
    if not plugin.update() then
        plugin.cleanup()
        active = false
    end
end

If your plugin needs a long load, spread it over several frames (load, init, then plugin.call(0, dataDir)) so the screen keeps updating.

The Lua side of the plugin API is also listed in System: native plugins.

Reference pages