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
- How a plugin works
- Required exports
- Lifecycle
- Loading a plugin from Lua
- Reference pages
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
- Building a plugin: toolchain, a complete example, build commands, deployment
- API reference: every
C8B_APIfield - Memory: the memory a plugin may use, cache and DMA rules
- Audio: the plugin audio queue
- Save states: the save-state
plugin_callconvention - API versions: what each API version added and how to stay compatible
- Memory map: the full console memory map