From dd7a569928514edb7f50db710b11cc85545eb8d1 Mon Sep 17 00:00:00 2001 From: John McCardle Date: Sat, 25 Oct 2025 21:01:17 +0000 Subject: [PATCH] Add "Adding-Python-Bindings" --- Adding-Python-Bindings.-.md | 435 ++++++++++++++++++++++++++++++++++++ 1 file changed, 435 insertions(+) create mode 100644 Adding-Python-Bindings.-.md diff --git a/Adding-Python-Bindings.-.md b/Adding-Python-Bindings.-.md new file mode 100644 index 0000000..7d9fc9c --- /dev/null +++ b/Adding-Python-Bindings.-.md @@ -0,0 +1,435 @@ +# 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 `PYTHON_BINDING_PATTERNS.md` (repository root) +- C++ class or function to expose + +## Quick Reference + +**Related Systems:** [[Python-Binding-Layer]], [[UI-Component-Hierarchy]] + +**Key Files:** +- `src/McRFPy_API.cpp` - Module-level functions +- `src/PyObjectUtils.h` - Helper utilities +- Individual `src/UI*.cpp` files - Type bindings + +**Documentation Format:** See CLAUDE.md "Inline C++ Documentation Format" + +--- + +## Workflow 1: Adding a Property to Existing Class + +### Step 1: Add to PyGetSetDef Array + +Find the class's `getsetters` array (e.g., `PyUISprite::getsetters`): + +```cpp +PyGetSetDef PyUISprite::getsetters[] = { + // Existing properties... + + // Add new property + {"rotation", (getter)PyUISprite::get_rotation, (setter)PyUISprite::set_rotation, + "Sprite rotation angle in degrees. Range: 0-360.", NULL}, + + {NULL} // Sentinel - always last! +}; +``` + +### Step 2: Implement Getter Function + +```cpp +PyObject* PyUISprite::get_rotation(PyUISprite* 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 + +```cpp +int PyUISprite::set_rotation(PyUISprite* 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; // Success +} +``` + +### Step 4: Add Documentation + +Update the docstring in PyGetSetDef: + +```cpp +{"rotation", (getter)PyUISprite::get_rotation, (setter)PyUISprite::set_rotation, + "Sprite rotation angle in degrees.\n\n" + "Range: 0-360 degrees. 0 is upright, increases clockwise.\n\n" + "Example:\n" + " sprite.rotation = 90 # Rotate 90 degrees clockwise\n\n" + "Note:\n" + " Rotation is applied during rendering, not to position.", + NULL}, +``` + +### Step 5: Rebuild and Test + +```bash +make clean && make + +cd build +./mcrogueface --exec test_rotation.py +``` + +--- + +## Workflow 2: Adding a Method to Existing Class + +### Step 1: Add to PyMethodDef Array + +Find the class's `methods` array: + +```cpp +PyMethodDef PyUIGrid::methods[] = { + // Existing methods... + + {"fill_rect", (PyCFunction)PyUIGrid::fill_rect, METH_VARARGS | METH_KEYWORDS, + "fill_rect(x: int, y: int, w: int, h: int, tile: int) -> None\n\n" + "Fill rectangular area with tile index.\n\n" + "Args:\n" + " x: Top-left X coordinate\n" + " y: Top-left Y coordinate\n" + " w: Width in tiles\n" + " h: Height in tiles\n" + " tile: Tile sprite index\n\n" + "Example:\n" + " grid.fill_rect(5, 5, 10, 10, 42) # Fill 10x10 area with tile 42"}, + + {NULL} // Sentinel +}; +``` + +### Step 2: Implement Method Function + +```cpp +PyObject* PyUIGrid::fill_rect(PyUIGrid* self, PyObject* args, PyObject* kwds) { + if (!self->data) { + PyErr_SetString(PyExc_RuntimeError, "Grid data is null"); + return NULL; + } + + int x, y, w, h, tile; + static char* kwlist[] = {"x", "y", "w", "h", "tile", NULL}; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "iiiii", kwlist, + &x, &y, &w, &h, &tile)) { + return NULL; // PyArg functions set error automatically + } + + // Bounds checking + if (x < 0 || y < 0 || x + w > self->data->grid_x || y + h > self->data->grid_y) { + PyErr_SetString(PyExc_ValueError, "Rectangle out of grid bounds"); + return NULL; + } + + // Fill the rectangle + for (int dx = 0; dx < w; dx++) { + for (int dy = 0; dy < h; dy++) { + self->data->at(x + dx, y + dy).tilesprite = tile; + } + } + + Py_RETURN_NONE; +} +``` + +### Step 3: Test + +```python +import mcrfpy + +grid = mcrfpy.Grid(50, 50, 16, 16) +grid.texture = mcrfpy.createTexture("tiles.png") + +# Test new method +grid.fill_rect(10, 10, 5, 5, 42) + +# Verify +assert grid.at((10, 10)).tilesprite == 42 +print("Test passed!") +``` + +--- + +## Workflow 3: Creating New Python Type + +### Step 1: Define Python Type Structure + +In new header file (e.g., `src/UIButton.h`): + +```cpp +// Python object wrapper +typedef struct { + PyObject_HEAD + std::shared_ptr data; +} PyUIButtonObject; + +// Python type object +class PyUIButton { +public: + static PyTypeObject Type; + static PyGetSetDef getsetters[]; + static PyMethodDef methods[]; + + // Lifecycle + static PyObject* pynew(PyTypeObject* type, PyObject* args, PyObject* kwds); + static int pyinit(PyUIButtonObject* self, PyObject* args, PyObject* kwds); + static void dealloc(PyUIButtonObject* self); + + // Properties + static PyObject* get_text(PyUIButtonObject* self, void* closure); + static int set_text(PyUIButtonObject* self, PyObject* value, void* closure); + // ... more properties +}; +``` + +### Step 2: Implement Type Object + +In `src/UIButton.cpp`: + +```cpp +PyTypeObject PyUIButton::Type = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "mcrfpy.Button", + .tp_basicsize = sizeof(PyUIButtonObject), + .tp_dealloc = (destructor)PyUIButton::dealloc, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_doc = "Button UI element with click handling", + .tp_methods = PyUIButton::methods, + .tp_getset = PyUIButton::getsetters, + .tp_new = PyUIButton::pynew, + .tp_init = (initproc)PyUIButton::pyinit, +}; +``` + +### Step 3: Register in Module + +In `src/McRFPy_API.cpp::PyInit_mcrfpy()`: + +```cpp +// After other type registrations +if (PyType_Ready(&PyUIButton::Type) < 0) { + return NULL; +} + +Py_INCREF(&PyUIButton::Type); +if (PyModule_AddObject(m, "Button", (PyObject*)&PyUIButton::Type) < 0) { + Py_DECREF(&PyUIButton::Type); + Py_DECREF(m); + return NULL; +} +``` + +### Step 4: Implement Constructor + +```cpp +PyObject* PyUIButton::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds) { + PyUIButtonObject* self = (PyUIButtonObject*)type->tp_alloc(type, 0); + if (self != NULL) { + self->data = nullptr; // Initialize in __init__ + } + return (PyObject*)self; +} + +int PyUIButton::pyinit(PyUIButtonObject* self, PyObject* args, PyObject* kwds) { + int x = 0, y = 0, w = 100, h = 30; + const char* text = ""; + + static char* kwlist[] = {"x", "y", "w", "h", "text", NULL}; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|iiiis", kwlist, + &x, &y, &w, &h, &text)) { + return -1; + } + + self->data = std::make_shared(x, y, w, h, text); + return 0; +} +``` + +--- + +## Common Patterns & Helpers + +### PyArgHelpers for Position/Size + +Use standardized helpers for tuple support: + +```cpp +#include "PyArgHelpers.h" + +// Accept (x, y) tuple OR separate x, y args +int x, y; +if (!PyArgParseTuple_IntIntHelper(args, kwds, x, y, "position", "x", "y")) { + return -1; +} +``` + +**See:** `src/PyArgHelpers.h` for complete helper reference + +### Closure Parameter Encoding + +For UIDrawable-derived types: + +```cpp +// Encode type and member index +(void*)((intptr_t)PyObjectsEnum::BUTTON << 8 | BUTTON_MEMBER_TEXT) +``` + +**See:** `PYTHON_BINDING_PATTERNS.md` for complete encoding scheme + +### Error Handling + +**Always check for NULL:** +```cpp +if (!self->data) { + PyErr_SetString(PyExc_RuntimeError, "Object data is null"); + return NULL; // Or -1 for setters +} +``` + +**Set descriptive errors:** +```cpp +PyErr_Format(PyExc_ValueError, "Index %d out of range (0-%d)", idx, max_idx); +``` + +--- + +## Testing Your Bindings + +### Manual Testing + +```python +#!/usr/bin/env python3 +import mcrfpy + +# Test property +sprite = mcrfpy.Sprite("test.png", 0, 0) +sprite.rotation = 45 +assert sprite.rotation == 45 + +# Test method +grid = mcrfpy.Grid(10, 10, 16, 16) +grid.fill_rect(0, 0, 5, 5, 42) +assert grid.at((0, 0)).tilesprite == 42 + +print("All tests passed!") +``` + +### Automated Testing + +Create test in `tests/test_new_binding.py`: + +```python +import mcrfpy +import sys + +def test_binding(): + # Your tests here + pass + +if __name__ == "__main__": + try: + test_binding() + print("PASS") + sys.exit(0) + except Exception as e: + print(f"FAIL: {e}") + sys.exit(1) +``` + +Run: `./build/mcrogueface --headless --exec tests/test_new_binding.py` + +--- + +## Common Pitfalls + +### Pitfall 1: Forgetting NULL Sentinel + +```cpp +// WRONG - missing sentinel +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`: + +```cpp +// WRONG - loses derived type +return (PyObject*)item->data.get(); + +// CORRECT - preserves type +RET_PY_INSTANCE(item->data); +``` + +### Pitfall 3: Missing Error Checks + +```cpp +// WRONG - no error check +double value = PyFloat_AsDouble(obj); + +// CORRECT +if (!PyFloat_Check(obj)) { + PyErr_SetString(PyExc_TypeError, "Expected float"); + return NULL; +} +double value = PyFloat_AsDouble(obj); +``` + +--- + +## Related Documentation + +- [[Python-Binding-Layer]] - System architecture +- `PYTHON_BINDING_PATTERNS.md` - Complete pattern reference +- CLAUDE.md - Inline documentation format +- [Python C API Reference](https://docs.python.org/3/c-api/) + +## Next Steps + +After adding bindings: +1. Rebuild: `make clean && make` +2. Test manually +3. Add automated test in `tests/` +4. Regenerate stub files: `./build/mcrogueface --exec tools/generate_stubs.py` +5. Update API docs: `./build/mcrogueface --exec tools/generate_dynamic_docs.py` +6. Document in wiki if new system \ No newline at end of file