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¶
- At startup the engine scans the plugins directory for
*.sofiles. - For each library it calls
luaopen_orca_<Name>(L)— the function that pyphp generates in*_export.c. - That generated function calls the
on-luaopencallback declared in the XML (e.g.on_ui_module_registered). - The callback calls
OBJ_RegisterClass()for each component type the plugin provides. - 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¶
- Create
plugins/MyPlugin/with aMyPlugin.xmlfile. - Run
cd tools && maketo generate the bindings. - Implement the component
.cfiles (see Object + Component System). - Add the plugin to
tools/MakefileunderMODULES(andDOC_MODULESif you want docs generated). - Build with
make buildpluginsfrom 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:
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
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: