Skip to content

Plugin System

ORCA supports two distinct kinds of plugins that can extend the engine at runtime:

Kind Language Example
C Component Plugin C, compiled to a shared library UIKit, SceneKit, SpriteKit
Lua Script Plugin Lua (or MoonScript) Routing, data-binding modules

C Component Plugins

C component plugins are shared libraries (.so on Linux/macOS, .dll on Windows) that register one or more new ClassDesc component types into the engine. The engine discovers and loads them at startup.

Plugin file layout

Every plugin follows the same layout as a core module:

plugins/UIKit/
├── UIKit.xml           # API definition — source of truth
├── UIKit.h             # Generated C header
├── UIKit_export.c      # Generated Lua bindings + luaopen_orca_UIKit()
├── UIKit_properties.h  # Generated property hash constants
├── UIKit.c             # on_ui_module_registered() callback
├── Button.c            # One .c file per component
├── Label.c
├── ImageView.c
└── …

The hand-written files are UIKit.c and the per-component *.c files. Everything else is generated by the pyphp toolchain from UIKit.xml.

Plugin loading flow

  1. At startup the engine scans the plugins directory for *.so files.
  2. For each library it calls luaopen_orca_<Name>(L) — the function that pyphp generates in *_export.c.
  3. That generated function calls the on-luaopen callback declared in the XML (e.g. on_ui_module_registered).
  4. The callback calls OBJ_RegisterClass() for each component type the plugin provides.
  5. The Lua state now has a new table (e.g. UIKit) with constructor functions for every registered component.
-- After UIKit is loaded:
local btn = UIKit.Button()
btn.Text    = "Click me"
btn.OnClick = function() print("clicked!") end

Message handling

Each component's ObjProc function receives messages dispatched by the engine:

LRESULT Button_Proc(lpObject_t obj, uint32_t msg,
                    wParam_t wParam, lParam_t lParam) {
    struct Button *self = GetButton(obj);
    switch (msg) {
        case kMsgCreate:   /* allocate resources */   break;
        case kMsgDestroy:  /* free resources */       break;
        case kMsgDraw:     /* submit draw calls */    break;
        case kMsgMouseUp:    /* fire Lua callback */    break;
    }
    return 0;
}

Message IDs are uint32_t constants (FNV1a hashes) declared in source/core/core_properties.h. Custom events can be added by any module via the <message> XML element.

Inheritance via ParentClasses

Set ParentClasses in the ClassDesc to inherit properties and default message handling from base classes:

static ClassDesc ButtonClass = {
    .ClassName     = "Button",
    .ClassID       = ID_Button,
    .ParentClasses = { ID_Node2D, 0 },  /* inherit layout + rendering from Node2D */
    .ClassSize     = sizeof(struct Button),
    .Defaults      = &button_defaults,
    .ObjProc       = Button_Proc,
};

When OBJ_AddComponent(obj, ID_Button) is called, the engine also attaches Node2D (and any of its parents) if they are not already present on the object.

Adding a new C component plugin

  1. Create plugins/MyPlugin/ with a MyPlugin.xml file.
  2. Run cd tools && make to generate the bindings.
  3. Implement the component .c files (see Object + Component System).
  4. Add the plugin to tools/Makefile under MODULES (and DOC_MODULES if you want docs generated).
  5. Build with make buildplugins from the project root.

System-Level Message Handlers

In addition to component-level ObjProc handlers (which process object messages), a plugin can register a system-level message handler that receives low-level platform events before they are dispatched to the object tree. This is the correct place to handle platform events that are not tied to a specific object (e.g. window resize, keyboard shortcuts, HTTP server commands).

The API

// source/sysutil/queue.c
bool_t SV_RegisterMessageProc(LRESULT (*proc)(lua_State*, struct AXmessage*));
bool_t SV_UnregisterMessageProc(LRESULT (*proc)(lua_State*, struct AXmessage*));

Handlers are called in reverse registration order (last registered = first called). Return TRUE to consume the event and stop further processing; return FALSE to let remaining handlers run.

Handler signature

LRESULT my_handle_event(lua_State *L, struct AXmessage *msg) {
    switch (msg->message) {
        case kEventKeyDown:
            /* handle key press, return TRUE to consume */
            return TRUE;
        default:
            return FALSE;   /* pass through to the next handler */
    }
}

The struct AXmessage fields used most often:

Field Type Meaning
message uint32_t Event type — one of the kEvent* constants in libs/platform
wParam wParam_t Primary parameter (key code, mouse button, etc.)
lParam lParam_t Secondary parameter (pointer to aux data)
target lua_State* Lua coroutine associated with the event (may be NULL)

Registration

Register from the module's on-luaopen callback (the function invoked from luaopen_orca_<Name>):

// In MyPlugin.c — called from luaopen_orca_MyPlugin (generated in MyPlugin_export.c)
void on_myplugin_registered(lua_State *L) {
    SV_RegisterMessageProc(my_handle_event);
}

For a conditional registration (e.g. only when a feature flag is set):

void on_myplugin_registered(lua_State *L) {
    lua_getglobal(L, "MY_FEATURE");
    if (lua_toboolean(L, -1)) {
        SV_RegisterMessageProc(my_handle_event);
        axPostMessageW(NULL, kEventReadCommands, 0, NULL);  /* seed the event pump if needed */
    }
    lua_pop(L, 1);
}

Examples in the codebase

Handler Location Purpose
CORE_ProcessMessage source/core/core_main.c Keyboard shortcuts, window paint/resize, coroutine lifecycle
ui_handle_event plugins/UIKit/UIKit_message.c Mouse, keyboard, and coroutine events for the UI object tree
filesystem_handle_event source/editor/server.c HTTP-style editor command server (kEventReadCommands loop)

Core — keyboard + window events

// source/core/core_main.c
LRESULT CORE_ProcessMessage(lua_State *L, struct AXmessage *e) {
    switch (e->message) {
        case kEventWindowPaint:
        case kEventWindowResized:
            core.realtime = axGetMilliseconds();
            core.frame++;
            return FALSE;   /* don't consume — other handlers (UIKit) still need it */
        case kEventKeyDown:
            /* look up shortcut and execute bound command */
            return FALSE;
        default:
            return FALSE;
    }
}

// Registered during module init:
SV_RegisterMessageProc(CORE_ProcessMessage);

UIKit — routing platform input to UI nodes

// plugins/UIKit/UIKit_message.c
LRESULT ui_handle_event(lua_State *L, struct AXmessage *msg) {
    switch (msg->message) {
        case kEventLeftMouseDown:
        case kEventLeftMouseUp:
        case kEventMouseMoved:
            return UI_HandleMouseEvent(L, msg->target, msg);  /* TRUE if consumed by a widget */
        case kEventKeyDown:
        case kEventChar:
            return UI_HandleKeyEvent(L, msg);
        default:
            return FALSE;
    }
}

// Registered in on_ui_module_registered() called from luaopen_orca_UIKit:
SV_RegisterMessageProc(ui_handle_event);

Editor server — reading HTTP commands

// source/editor/server.c
LRESULT filesystem_handle_event(lua_State *L, struct AXmessage *msg) {
    if (msg->message != kEventReadCommands) return FALSE;
    LPSTR url = UI_ReadClientCommands();
    if (!url) exit(0);
    /* parse URL, dispatch to route handlers, write XML response to stdout */
    axPostMessageW(msg->target, kEventReadCommands, 0, NULL);  /* re-arm for next request */
    return TRUE;
}

// Registered conditionally from luaopen_orca_editor when SERVER global is set:
lua_getglobal(L, "SERVER");
if (lua_toboolean(L, -1)) {
    SV_RegisterMessageProc(filesystem_handle_event);
    axPostMessageW(NULL, kEventReadCommands, 0, NULL);   /* post the first event to start loop */
}
lua_pop(L, 1);

Lua Script Plugins

Lua script plugins are pure-Lua modules that extend the engine's behaviour without any native code. They are loaded using the standard Lua require mechanism:

local router = require "orca.routing"
local ui     = require "orca.ui"

Lua plugins can:

  • Call any API exposed by the engine and by C component plugins
  • Define new Lua classes (metatables) that wrap or extend engine objects
  • Implement application-level concerns: routing, data binding, state management, etc.

Script plugins live in the project directory or anywhere on Lua's package.path. They are loaded by the project's entry-point Lua file (usually main.lua or Application.lua).

Example — a simple Lua plugin

-- orca/myplugin.lua
local M = {}

function M.greet(name)
    print("Hello from MyPlugin, " .. name .. "!")
end

return M
-- main.lua
local myplugin = require "orca.myplugin"
myplugin.greet("world")

UIKit: A Worked Example

UIKit is the primary UI framework for ORCA and is the most complete example of a C component plugin. It provides 16+ component types:

Component Purpose
Button Tappable button with optional icon and label
Label Single-line text display
TextBlock Multi-line text with wrapping, overflow, and alignment
ImageView Image display with stretch modes
Grid Grid layout container
StackView Linear stack layout (horizontal or vertical)
Input Text input field
Form Form container with validation
PageHost Tabbed / paged navigation container
Screen Root-level fullscreen container

All components inherit from Node2D, which provides 2D layout, transform, and rendering via the renderer module.

UIKit components are declared in XML and used from Lua or from ORCA XML project files:

<!-- XML usage -->
<Screen Name="Main" Width="1024" Height="768">
  <StackView Name="Layout">
    <Button Name="OkBtn" Text="OK" Width="120" Height="40"/>
  </StackView>
</Screen>
-- Lua usage
local screen = UIKit.Screen()
local btn    = UIKit.Button()
btn.Text     = "OK"
btn.Width    = 120
btn.Height   = 40
screen:AddChild(btn)