Page:
Adding Python Bindings
Pages
AI and Pathfinding
Adding Python Bindings
Animation System
Design Proposals
Development Workflow
Entity Management
Grid Interaction Patterns
Grid Rendering Pipeline
Grid System
Grid TCOD Integration
Headless Mode
Home
Input and Events
Issue Roadmap
LLM Agent Testbed Architecture
Performance Optimization Workflow
Performance and Profiling
Procedural-Generation
Proposal: Next Generation Grid & Entity System
Python Binding Layer
Rendering and Visuals
Strategic Direction
UI Component Hierarchy
UI Widget Patterns
Writing Tests
No results
3
Adding Python Bindings
John McCardle edited this page 2026-02-07 23:47:56 +00:00
Table of Contents
- Adding Python Bindings
- Prerequisites
- Quick Reference
- Workflow 1: Adding a Property to Existing Class
- Step 1: Add to PyGetSetDef Array
- Step 2: Implement Getter Function
- Step 3: Implement Setter Function
- Step 4: Rebuild and Test
- Workflow 2: Adding a Method to Existing Class
- Workflow 3: Creating New Python Type
- Documentation Macros
- Testing Your Bindings
- Regenerating Documentation
- Common Pitfalls
- Pitfall 1: Forgetting NULL Sentinel
- Pitfall 2: Type Preservation in Collections
- Pitfall 3: Missing Error Checks
- Pitfall 4: Not Setting tp_methods/tp_getset Before PyType_Ready
- Related Documentation
Adding Python Bindings
Step-by-step guide for exposing C++ functionality to Python in McRogueFace.
Prerequisites
- Understanding of Python-Binding-Layer system architecture
- Familiarity with C++ class or function to expose
- McRogueFace builds from project root with
make
Quick Reference
Related Systems: Python-Binding-Layer, UI-Component-Hierarchy
Key Files:
src/McRFPy_API.cpp- Module definition and type registrationsrc/McRFPy_Doc.h- Documentation macros (MCRF_METHOD, MCRF_PROPERTY, etc.)src/PyObjectUtils.h- Helper utilities- Individual
src/Py*.cppfiles - Type bindings
Workflow 1: Adding a Property to Existing Class
Step 1: Add to PyGetSetDef Array
Find the class's getsetters array:
PyGetSetDef UISprite::getsetters[] = {
// Existing properties...
// Add new property with doc macro
{"rotation", (getter)UISprite::get_rotation, (setter)UISprite::set_rotation,
MCRF_PROPERTY(rotation,
"Sprite rotation angle in degrees. Range: 0-360."
), NULL},
{NULL} // Sentinel - always last!
};
Step 2: Implement Getter Function
PyObject* UISprite::get_rotation(PyUISpriteObject* self, void* closure) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "UISprite data is null");
return NULL;
}
return PyFloat_FromDouble(self->data->rotation);
}
Step 3: Implement Setter Function
int UISprite::set_rotation(PyUISpriteObject* self, PyObject* value, void* closure) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "UISprite data is null");
return -1;
}
if (!PyFloat_Check(value) && !PyLong_Check(value)) {
PyErr_SetString(PyExc_TypeError, "rotation must be a number");
return -1;
}
double rotation = PyFloat_AsDouble(value);
if (rotation < 0 || rotation > 360) {
PyErr_SetString(PyExc_ValueError, "rotation must be 0-360");
return -1;
}
self->data->rotation = rotation;
return 0;
}
Step 4: Rebuild and Test
# From project root
make clean && make
cd build
./mcrogueface --headless -c "
import mcrfpy, sys
s = mcrfpy.Sprite(x=0, y=0, sprite_index=0)
s.rotation = 45
assert s.rotation == 45
print('PASS')
sys.exit(0)
"
Workflow 2: Adding a Method to Existing Class
Step 1: Add to PyMethodDef Array
PyMethodDef UIGrid::methods[] = {
// Existing methods...
{"fill_walkable", (PyCFunction)UIGrid::fill_walkable, METH_VARARGS | METH_KEYWORDS,
MCRF_METHOD(Grid, fill_walkable,
MCRF_SIG("(walkable: bool)", "None"),
MCRF_DESC("Set walkable property for all cells."),
MCRF_ARGS_START
MCRF_ARG("walkable", "Whether cells should be walkable")
)},
{NULL} // Sentinel
};
Step 2: Implement Method
PyObject* UIGrid::fill_walkable(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Grid data is null");
return NULL;
}
int walkable;
static char* kwlist[] = {"walkable", NULL};
if (!PyArg_ParseTupleAndKeywords(args, kwds, "p", kwlist, &walkable)) {
return NULL;
}
for (int x = 0; x < self->data->grid_x; x++) {
for (int y = 0; y < self->data->grid_y; y++) {
self->data->at(x, y).walkable = walkable;
}
}
Py_RETURN_NONE;
}
Step 3: Test
import mcrfpy
import sys
grid = mcrfpy.Grid(grid_size=(10, 10), pos=(0, 0), size=(160, 160))
grid.fill_walkable(True)
assert grid.at(5, 5).walkable == True
print("PASS")
sys.exit(0)
Workflow 3: Creating New Python Type
Step 1: Define Type Structure
In new header file (e.g., src/PyMyType.h):
typedef struct {
PyObject_HEAD
std::shared_ptr<MyType> data;
} PyMyTypeObject;
namespace mcrfpydef {
static PyTypeObject PyMyTypeType = {
.tp_name = "mcrfpy.MyType",
.tp_basicsize = sizeof(PyMyTypeObject),
// ... filled in during init
};
}
class PyMyType {
public:
static PyGetSetDef getsetters[];
static PyMethodDef methods[];
static int init(PyMyTypeObject* self, PyObject* args, PyObject* kwds);
static PyObject* pynew(PyTypeObject* type, PyObject* args, PyObject* kwds);
static void dealloc(PyMyTypeObject* self);
};
Step 2: Implement (in src/PyMyType.cpp)
#include "PyMyType.h"
#include "McRFPy_Doc.h"
int PyMyType::init(PyMyTypeObject* self, PyObject* args, PyObject* kwds) {
// Parse arguments, initialize self->data
self->data = std::make_shared<MyType>();
return 0;
}
PyObject* PyMyType::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds) {
auto* self = (PyMyTypeObject*)type->tp_alloc(type, 0);
if (self) self->data = nullptr;
return (PyObject*)self;
}
void PyMyType::dealloc(PyMyTypeObject* self) {
self->data.reset();
Py_TYPE(self)->tp_free((PyObject*)self);
}
PyGetSetDef PyMyType::getsetters[] = {
{"name", (getter)get_name, (setter)set_name,
MCRF_PROPERTY(name, "Object name."), NULL},
{NULL}
};
PyMethodDef PyMyType::methods[] = {
{NULL}
};
Step 3: Register in Module
In src/McRFPy_API.cpp:
#include "PyMyType.h"
// In the exported_types or internal_types array:
static PyTypeObject* exported_types[] = {
// ... existing types ...
&mcrfpydef::PyMyTypeType,
};
// Before PyType_Ready calls:
mcrfpydef::PyMyTypeType.tp_methods = PyMyType::methods;
mcrfpydef::PyMyTypeType.tp_getset = PyMyType::getsetters;
mcrfpydef::PyMyTypeType.tp_init = (initproc)PyMyType::init;
mcrfpydef::PyMyTypeType.tp_new = PyMyType::pynew;
mcrfpydef::PyMyTypeType.tp_dealloc = (destructor)PyMyType::dealloc;
CMake auto-discovers new .cpp files via GLOB_RECURSE, so no CMakeLists.txt changes needed.
Documentation Macros
Use macros from src/McRFPy_Doc.h for all Python-facing documentation:
| Macro | Purpose |
|---|---|
MCRF_METHOD(cls, name, ...) |
Method docstring |
MCRF_PROPERTY(name, desc) |
Property docstring |
MCRF_SIG(params, ret) |
Method signature |
MCRF_DESC(text) |
Description paragraph |
MCRF_ARGS_START |
Begin arguments section |
MCRF_ARG(name, desc) |
Individual argument |
MCRF_RETURNS(text) |
Return value description |
MCRF_RAISES(exc, cond) |
Exception documentation |
MCRF_NOTE(text) |
Important notes |
MCRF_LINK(path, text) |
External documentation link |
Testing Your Bindings
Direct Execution Test
import mcrfpy
import sys
# Test property
s = mcrfpy.Sprite(x=0, y=0, sprite_index=0)
s.rotation = 45
assert s.rotation == 45
# Test method
grid = mcrfpy.Grid(grid_size=(10, 10), pos=(0, 0), size=(160, 160))
grid.fill_walkable(True)
assert grid.at(5, 5).walkable == True
print("PASS")
sys.exit(0)
Run
cd build
./mcrogueface --headless --exec ../tests/unit/my_binding_test.py
Regenerating Documentation
After adding bindings:
# From project root
make
# Generate all docs (recommended)
./tools/generate_all_docs.sh
# Or individually:
cd build
./mcrogueface --headless --exec ../tools/generate_dynamic_docs.py
./mcrogueface --headless --exec ../tools/generate_stubs_v2.py
Common Pitfalls
Pitfall 1: Forgetting NULL Sentinel
// WRONG - missing sentinel causes crash
PyGetSetDef getsetters[] = {
{"x", get_x, set_x, "X position", NULL}
};
// CORRECT
PyGetSetDef getsetters[] = {
{"x", get_x, set_x, "X position", NULL},
{NULL} // Must have this!
};
Pitfall 2: Type Preservation in Collections
When returning from collections, use RET_PY_INSTANCE:
// WRONG - returns base UIDrawable wrapper
return generic_wrap(item->data);
// CORRECT - preserves derived type (Frame, Caption, etc.)
RET_PY_INSTANCE(item->data);
Pitfall 3: Missing Error Checks
// WRONG - no type check
double value = PyFloat_AsDouble(obj);
// CORRECT
if (!PyFloat_Check(obj) && !PyLong_Check(obj)) {
PyErr_SetString(PyExc_TypeError, "Expected number");
return NULL;
}
double value = PyFloat_AsDouble(obj);
Pitfall 4: Not Setting tp_methods/tp_getset Before PyType_Ready
// WRONG - setting after PyType_Ready has no effect
PyType_Ready(&MyType);
MyType.tp_methods = methods;
// CORRECT - set before PyType_Ready
MyType.tp_methods = methods;
MyType.tp_getset = getsetters;
PyType_Ready(&MyType);
Related Documentation
- Python-Binding-Layer - System architecture
- UI-Component-Hierarchy - Classes exposed to Python
src/McRFPy_Doc.h- Documentation macro reference- Python C API Reference