Updated 7 November 2026

Building a plugin

This page builds a small plugin from scratch, for the console and for the emulator, and runs it from a Lua game.

Contents

What you need

  • The Arm GNU toolchain (arm-none-eabi-gcc, arm-none-eabi-g++, arm-none-eabi-objcopy) for console builds.
  • A host compiler (clang, gcc or MSVC) for emulator builds.
  • From the plugin SDK (called $C8B_PLUGIN_SDK below):
    • include/: the headers c8b_plugin.h (helper macros) and core8bit_plugin.h (the C8B_API table and constants);
    • stm32/plugin.ld: the console linker script;
    • stm32/startup.cpp: the console startup code.

A minimal plugin

The plugin clears the screen, draws a green square that moves up while Up is held, and stops when Start is pressed. On the console the Menu key reports C8B_BTN_START.

// hello_plugin.cpp
#include "c8b_plugin.h"

C8B_PLUGIN_GLOBALS

static int box_y = 120;

C8B_PLUGIN_MAIN {                       // plugin_init
    c8b.log("hello: init\n");
    return true;                        // false makes plugin_init return -1
}

C8B_PLUGIN_UPDATE {                     // plugin_update, once per frame
    C8B_API* api = c8b.raw;             // full API table

    if (api->get_buttons_pressed() & C8B_BTN_START)
        return false;                   // tell Lua to call plugin.cleanup()

    if (api->get_buttons() & C8B_BTN_UP)
        box_y = (box_y > 0) ? box_y - 2 : api->screen_height - 32;

    api->clear(0x0000);                               // black, RGB565
    api->fill_rect(224, box_y, 32, 32, 0x07E0);       // green
    return true;
}

C8B_PLUGIN_CLEANUP {                    // plugin_cleanup
    c8b.log("hello: cleanup\n");
}

On the console, C8B_PLUGIN_MAIN first zeroes .bss, sets up the heap, runs C++ constructors and copies the ITCM/DTCM sections, then runs your code and stores the api pointer. c8b_plugin.h also offers a small c8b helper object (c8b.log, c8b.getFramebuffer() and others) next to the full table.

Building for the console

The console runs a raw binary linked at the fixed address 0xC1620000. Do not use -fPIC.

SDK=$C8B_PLUGIN_SDK
CPU="-mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard"
CXXFLAGS="$CPU -O2 -ffunction-sections -fdata-sections \
  -std=c++17 -fno-exceptions -fno-rtti -fno-threadsafe-statics \
  -DPLUGIN_STM32 -I$SDK/include"

arm-none-eabi-g++ $CXXFLAGS -c hello_plugin.cpp     -o hello_plugin.o
arm-none-eabi-g++ $CXXFLAGS -c $SDK/stm32/startup.cpp -o startup.o

arm-none-eabi-g++ $CPU -T $SDK/stm32/plugin.ld \
  -Wl,--gc-sections -Wl,--print-memory-usage \
  --specs=nano.specs --specs=nosys.specs \
  hello_plugin.o startup.o -o hello_plugin.elf

arm-none-eabi-objcopy -O binary hello_plugin.elf hello_plugin.bin
Flag Purpose
-mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard Required: the console ABI.
-DPLUGIN_STM32 Selects the console startup path in c8b_plugin.h. Use it only for console builds.
-ffunction-sections -fdata-sections, -Wl,--gc-sections Removes unused code and data.
-fno-exceptions -fno-rtti -fno-threadsafe-statics No exception unwinder or RTTI is available in the plugin.
--specs=nano.specs --specs=nosys.specs Small C library without an operating system.
-T plugin.ld Places the header, code and data at the plugin slot addresses.
-Wl,--print-memory-usage Prints how full ITCM, DTCM and the slot are.

-O2 is a good default; -O3 and -ffast-math also work.

Building for the emulator

For the emulator, build the same sources as a shared library with the host compiler. Do not define PLUGIN_STM32 and do not link startup.cpp or plugin.ld.

macOS:

clang++ -std=c++17 -O2 -fPIC -I$C8B_PLUGIN_SDK/include \
  -dynamiclib -undefined dynamic_lookup \
  hello_plugin.cpp -o hello_plugin.dylib.new
codesign --force --sign - hello_plugin.dylib.new
mv hello_plugin.dylib.new hello_plugin.dylib

Re-sign the library after every build and replace the file (mv) instead of overwriting it in place; macOS refuses to load a signed library that was modified in place.

Linux:

g++ -std=c++17 -O2 -fPIC -shared -I$C8B_PLUGIN_SDK/include \
  hello_plugin.cpp -o hello_plugin.so

Windows (MinGW or MSVC): build hello_plugin.dll from the same source. The plugin functions are exported by C8B_EXPORT (__declspec(dllexport) on Windows), so no .def file is needed:

x86_64-w64-mingw32-g++ -shared -O2 -I$C8B_PLUGIN_SDK/include -o hello_plugin.dll hello_plugin.cpp

Installing and running it

Put the plugin and a main.lua in a game folder:

/Games/Hello/
    main.lua
    plugins/
        hello_plugin.bin      console build
        hello_plugin.dylib    emulator build (.so on Linux, .dll on Windows)
-- Games/Hello/main.lua
local active = false

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

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

Use the .bin path on both platforms: the emulator replaces .bin with the host library extension. Copy the folder to /Games/Hello/ on the SD card and start it from the launcher, or run it in the emulator with core8bit_emulator --game Hello.

Common mistakes

  • Touching memory before startup. The console loader does not zero .bss or copy ITCM/DTCM code. C8B_PLUGIN_MAIN (with stm32/startup.cpp) does it before your code runs. If you write your own plugin_init, do these steps first and touch nothing in .bss, ITCM or DTCM before the matching step.
  • Position-independent code. Console plugins only run at 0xC1620000. Link with plugin.ld, never with -fPIC.
  • Missing header. The .bin must start with the 48-byte header with magic C8P1 (0x31503843) and non-NULL init, update and cleanup entries. plugin.ld keeps the .plugin_header section first; do not strip it and always produce the .bin with objcopy -O binary. The header is defined in the SDK's startup.cpp; link it into every plugin.
  • Slot overflow. Image, .bss and heap share the 5 632 KB slot that ends at 0xC1BA0000. The template heap stops 4 KB below that address.
  • Too much DTCM. The plugin DTCM area is 40 KB. The emulator rejects a plugin whose DTCM sections are larger.
  • ITCM below 0x800. The first 2 KB of ITCM belong to the system. Start plugin ITCM at 0x800.
  • Stale macOS library. Re-sign with codesign --force --sign - and replace the file rather than overwriting it.
  • Newer API functions. Check api->version and the pointer before calling anything added after v1: if (api->version >= 7u && api->present_set_async && api->present_wait).

Image layout

The .bin contains everything from 0xC1620000 to the end of .data. .bss and the heap are not in the file.

file offset  VMA                         section
0x000        0xC1620000 SDRAM            .plugin_header (48 B)
0x030        0x00000800 ITCM             .itcm_hot      (copied at startup)
             0x0000C000 ITCM             ITCM phase overlays (optional)
             0x3003FF40 D2               .d2_hot        (optional)
             SDRAM, in place             .text
             0x20016000 DTCM             .dtcm_hot      (copied at startup)
             SDRAM, in place             .rodata, .ARM.exidx, init/fini arrays, .data
(not in file) SDRAM                      .bss (zeroed at startup)
(not in file) end of .bss to 0xC1BA0000 - 4 KB   heap

.data needs no copy: it is linked at its load address. A .dtcm_bss section is not loaded and must be zeroed by the plugin.

Memory regions in stm32/plugin.ld:

Region Start Size
CARTRIDGE (plugin slot) 0xC1620000 5 632 KB
ITCM_PLUGIN 0x00000800 62 KB
DTCM_PLUGIN 0x20016000 40 KB

Plugin header:

Offset Field Value
0x00 magic 0x31503843 ("C8P1")
0x04 version 0x00010000
0x08 init address of plugin_init (required)
0x0C update address of plugin_update (required)
0x10 cleanup address of plugin_cleanup (required)
0x14 call address of plugin_call or 0
0x18 get_exports address of plugin_get_exports or 0
0x1C–0x2F reserved[5] 0

Placing code in fast memory

Code in SDRAM runs in place. Hot code and data run faster from ITCM and DTCM; the linker script collects the input sections .itcm_hot* and .dtcm_hot*, and the startup code copies them at init.

  • Mark functions with __attribute__((section(".itcm_hot"))) and data with __attribute__((section(".dtcm_hot"))).
  • Or list whole object files in the linker script, for example *r_draw.o(.text .text.*). Compile these objects without LTO.
  • Or rename a whole object's code: arm-none-eabi-objcopy --rename-section .text=.itcm_hot file.o.

The copy uses word stores and ends with dsb; isb, so no ITCM function (an ITCM memcpy included) runs before it.

ITCM phase overlays (API v4)

When the hot code is larger than 62 KB, split ITCM into 46 KB resident code and one 16 KB window at 0x0000C000 (C8B_PLUGIN_ITCM_OVERLAY_VMA, C8B_PLUGIN_ITCM_OVERLAY_SIZE). In the linker script, put the phases in an OVERLAY 0x0000C000 : NOCROSSREFS block. Swap a phase in with api->load_itcm_overlay(load_address, size) and keep every function that is called across phases in resident memory.

D2 code tail (FastBuffer DATA_PLUS_CODE)

A .d2_hot section can be linked at C8B_D2_CODE_VMA (0x3003FF40, 30 KB). At init:

  1. Call api->set_fast_buffer_mode(C8B_FB_MODE_DATA_PLUS_CODE).
  2. Check that get_fast_buffer() + get_fast_buffer_size() is at most the start of .d2_hot, and that .d2_hot ends within 30 KB of it.
  3. Call api->fast_buffer_wait().
  4. Copy the .d2_hot load image to 0x3003FF40.
  5. Call api->invalidate_icache(addr, size).

The FastBuffer then has 98 KB for data.