Update Python Binding Layer

John McCardle 2026-02-07 23:49:47 +00:00
commit 6beb3ee39f

@ -1,238 +1,238 @@
# Python Binding Layer # Python Binding Layer
The Python Binding Layer exposes C++ engine functionality to Python using Python's C API. This system allows game logic to be written in Python while maintaining C++ rendering performance. The Python Binding Layer exposes C++ engine functionality to Python using Python's C API. This system allows game logic to be written in Python while maintaining C++ rendering performance.
## Quick Reference ## Quick Reference
**Related Issues:** **Related Issues:**
- [#126](../issues/126) - Generate Perfectly Consistent Python Interface - [#126](../issues/126) - Generate Perfectly Consistent Python Interface
- [#109](../issues/109) - Vector Convenience Methods - [#109](../issues/109) - Vector Convenience Methods
- [#92](../issues/92) - Inline C++ Documentation System (Closed - Implemented) - [#92](../issues/92) - Inline C++ Documentation System (Closed - Implemented)
**Key Files:** **Key Files:**
- `src/McRFPy_API.h` / `src/McRFPy_API.cpp` - Main Python module definition - `src/McRFPy_API.h` / `src/McRFPy_API.cpp` - Main Python module definition
- `src/McRFPy_Doc.h` - Documentation macro system (MCRF_METHOD, MCRF_PROPERTY, etc.) - `src/McRFPy_Doc.h` - Documentation macro system (MCRF_METHOD, MCRF_PROPERTY, etc.)
- `src/PyObjectUtils.h` - Utility functions for Python/C++ conversion - `src/PyObjectUtils.h` - Utility functions for Python/C++ conversion
- `src/UIDrawable.h` - `RET_PY_INSTANCE` macro pattern - `src/UIDrawable.h` - `RET_PY_INSTANCE` macro pattern
- Individual class binding files: `src/Py*.cpp` - Individual class binding files: `src/Py*.cpp`
**Reference Documentation:** **Reference Documentation:**
- [[Adding-Python-Bindings]] - Step-by-step workflow guide - [[Adding-Python-Bindings]] - Step-by-step workflow guide
## Architecture Overview ## Architecture Overview
### Module Structure ### Module Structure
``` ```
mcrfpy (C extension module) mcrfpy (C extension module)
|-- Types |-- Types
| |-- UI: Frame, Caption, Sprite, Grid, Entity | |-- UI: Frame, Caption, Sprite, Grid, Entity
| |-- Geometry: Arc, Circle, Line | |-- Geometry: Arc, Circle, Line
| |-- Grid Layers: TileLayer, ColorLayer | |-- Grid Layers: TileLayer, ColorLayer
| |-- Data: Color, Vector, Texture, Font | |-- Data: Color, Vector, Texture, Font
| |-- Scene: Scene (with children, on_key) | |-- Scene: Scene (with children, on_key)
| |-- Timer: Timer (with stop, pause, resume) | |-- Timer: Timer (with stop, pause, resume)
| |-- Pathfinding: AStarPath, DijkstraMap | |-- Pathfinding: AStarPath, DijkstraMap
| |-- Enums: Key, MouseButton, InputState, Easing | |-- Enums: Key, MouseButton, InputState, Easing
| +-- Tiled: TileSetFile, WangSet, LdtkProject, AutoRuleSet | +-- Tiled: TileSetFile, WangSet, LdtkProject, AutoRuleSet
| |
|-- Module Functions |-- Module Functions
| |-- current_scene (property) | |-- current_scene (property)
| |-- step(dt) | |-- step(dt)
| |-- start_benchmark(), end_benchmark(), log_benchmark() | |-- start_benchmark(), end_benchmark(), log_benchmark()
| +-- find() (scene lookup) | +-- find() (scene lookup)
| |
+-- Submodules +-- Submodules
+-- automation (screenshots, mouse, keyboard) +-- automation (screenshots, mouse, keyboard)
``` ```
**Entry Point:** `src/McRFPy_API.cpp::PyInit_mcrfpy()` **Entry Point:** `src/McRFPy_API.cpp::PyInit_mcrfpy()`
### Binding Patterns ### Binding Patterns
#### Pattern 1: PyGetSetDef for Properties #### Pattern 1: PyGetSetDef for Properties
Properties exposed via getter/setter arrays: Properties exposed via getter/setter arrays:
```cpp ```cpp
PyGetSetDef UISprite::getsetters[] = { PyGetSetDef UISprite::getsetters[] = {
{"x", (getter)Drawable::get_member, (setter)Drawable::set_member, {"x", (getter)Drawable::get_member, (setter)Drawable::set_member,
MCRF_PROPERTY(x, "X coordinate of the sprite."), MCRF_PROPERTY(x, "X coordinate of the sprite."),
(void*)SPRITE_X}, (void*)SPRITE_X},
{"texture", (getter)UISprite::get_texture, (setter)UISprite::set_texture, {"texture", (getter)UISprite::get_texture, (setter)UISprite::set_texture,
MCRF_PROPERTY(texture, "Sprite texture reference."), MCRF_PROPERTY(texture, "Sprite texture reference."),
NULL}, NULL},
{NULL} // Sentinel {NULL} // Sentinel
}; };
``` ```
#### Pattern 2: PyMethodDef for Methods #### Pattern 2: PyMethodDef for Methods
Methods exposed via method definition arrays: Methods exposed via method definition arrays:
```cpp ```cpp
PyMethodDef UIGrid::methods[] = { PyMethodDef UIGrid::methods[] = {
{"at", (PyCFunction)UIGrid::at, METH_VARARGS | METH_KEYWORDS, {"at", (PyCFunction)UIGrid::at, METH_VARARGS | METH_KEYWORDS,
MCRF_METHOD(Grid, at, MCRF_METHOD(Grid, at,
MCRF_SIG("(x: int, y: int)", "GridPoint"), MCRF_SIG("(x: int, y: int)", "GridPoint"),
MCRF_DESC("Access grid cell at position."), MCRF_DESC("Access grid cell at position."),
MCRF_ARGS_START MCRF_ARGS_START
MCRF_ARG("x", "X coordinate") MCRF_ARG("x", "X coordinate")
MCRF_ARG("y", "Y coordinate") MCRF_ARG("y", "Y coordinate")
MCRF_RETURNS("GridPoint object at that position") MCRF_RETURNS("GridPoint object at that position")
)}, )},
{NULL} {NULL}
}; };
``` ```
#### Pattern 3: RET_PY_INSTANCE Macro #### Pattern 3: RET_PY_INSTANCE Macro
Converting C++ objects to Python requires type-aware allocation: Converting C++ objects to Python requires type-aware allocation:
```cpp ```cpp
RET_PY_INSTANCE(target); RET_PY_INSTANCE(target);
// Expands to switch on target->derived_type(): // Expands to switch on target->derived_type():
// - Allocates correct Python type (Frame, Caption, Sprite, Grid) // - Allocates correct Python type (Frame, Caption, Sprite, Grid)
// - Assigns shared_ptr to data member // - Assigns shared_ptr to data member
// - Returns PyObject* // - Returns PyObject*
``` ```
**File:** `src/UIDrawable.h` **File:** `src/UIDrawable.h`
## Documentation Macro System ## Documentation Macro System
Since October 2025, all Python-facing documentation uses macros from `src/McRFPy_Doc.h`: Since October 2025, all Python-facing documentation uses macros from `src/McRFPy_Doc.h`:
```cpp ```cpp
#include "McRFPy_Doc.h" #include "McRFPy_Doc.h"
// Method documentation // Method documentation
MCRF_METHOD(ClassName, method_name, MCRF_METHOD(ClassName, method_name,
MCRF_SIG("(arg: type)", "return_type"), MCRF_SIG("(arg: type)", "return_type"),
MCRF_DESC("What the method does."), MCRF_DESC("What the method does."),
MCRF_ARGS_START MCRF_ARGS_START
MCRF_ARG("arg", "Argument description") MCRF_ARG("arg", "Argument description")
MCRF_RETURNS("Return value description") MCRF_RETURNS("Return value description")
) )
// Property documentation // Property documentation
MCRF_PROPERTY(property_name, "Description of the property.") MCRF_PROPERTY(property_name, "Description of the property.")
``` ```
This ensures documentation stays in sync with code. See `tools/generate_dynamic_docs.py` for the extraction pipeline. This ensures documentation stays in sync with code. See `tools/generate_dynamic_docs.py` for the extraction pipeline.
## Common Patterns ## Common Patterns
### Type Preservation in Collections ### Type Preservation in Collections
**Challenge:** Shared pointers can lose Python type information when retrieved from collections. **Challenge:** Shared pointers can lose Python type information when retrieved from collections.
**Solution:** Use `RET_PY_INSTANCE` when returning from collections, which checks `derived_type()` to allocate the correct Python wrapper. **Solution:** Use `RET_PY_INSTANCE` when returning from collections, which checks `derived_type()` to allocate the correct Python wrapper.
### Constructor Keywords ### Constructor Keywords
All public types use keyword arguments: All public types use keyword arguments:
```python ```python
# UI types # UI types
frame = mcrfpy.Frame(pos=(100, 200), size=(300, 150)) frame = mcrfpy.Frame(pos=(100, 200), size=(300, 150))
caption = mcrfpy.Caption(text="Hello", pos=(10, 10)) caption = mcrfpy.Caption(text="Hello", pos=(10, 10))
sprite = mcrfpy.Sprite(x=50, y=50, sprite_index=0) sprite = mcrfpy.Sprite(x=50, y=50, sprite_index=0)
# Grid types # Grid types
grid = mcrfpy.Grid(grid_size=(50, 50), pos=(0, 0), size=(800, 600)) grid = mcrfpy.Grid(grid_size=(50, 50), pos=(0, 0), size=(800, 600))
entity = mcrfpy.Entity(grid_pos=(10, 10), sprite_index=42) entity = mcrfpy.Entity(grid_pos=(10, 10), sprite_index=42)
# Data types # Data types
color = mcrfpy.Color(255, 128, 0, 200) color = mcrfpy.Color(255, 128, 0, 200)
texture = mcrfpy.Texture("assets/sprites/tileset.png", 16, 16) texture = mcrfpy.Texture("assets/sprites/tileset.png", 16, 16)
# Scene and Timer # Scene and Timer
scene = mcrfpy.Scene("my_scene") scene = mcrfpy.Scene("my_scene")
timer = mcrfpy.Timer("my_timer", callback, 500) # callback(timer, runtime) timer = mcrfpy.Timer("my_timer", callback, 500) # callback(timer, runtime)
``` ```
### PyArgHelpers ### PyArgHelpers
Standardized argument parsing for tuples vs separate args: Standardized argument parsing for tuples vs separate args:
```cpp ```cpp
#include "PyArgHelpers.h" #include "PyArgHelpers.h"
// Accept both (x, y) and x, y formats // Accept both (x, y) and x, y formats
PyArgParseTuple_IntIntHelper(args, kwds, x, y, "position", "x", "y"); PyArgParseTuple_IntIntHelper(args, kwds, x, y, "position", "x", "y");
``` ```
## Key Subsystems ## Key Subsystems
### Scene System ### Scene System
Scenes are first-class Python objects: Scenes are first-class Python objects:
```python ```python
scene = mcrfpy.Scene("game") scene = mcrfpy.Scene("game")
scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(100, 100))) scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(100, 100)))
scene.on_key = lambda key, action: None # Key enum, InputState enum scene.on_key = lambda key, action: None # Key enum, InputState enum
mcrfpy.current_scene = scene mcrfpy.current_scene = scene
``` ```
### Animation System ### Animation System
Animation is a method on UIDrawable objects: Animation is a method on UIDrawable objects:
```python ```python
frame.animate("x", 500.0, 2.0, mcrfpy.Easing.EASE_IN_OUT) frame.animate("x", 500.0, 2.0, mcrfpy.Easing.EASE_IN_OUT)
frame.animate("opacity", 0.0, 1.0, mcrfpy.Easing.LINEAR, callback=on_done) frame.animate("opacity", 0.0, 1.0, mcrfpy.Easing.LINEAR, callback=on_done)
# callback receives (target, property_name, final_value) # callback receives (target, property_name, final_value)
``` ```
### Timer System ### Timer System
Timers are objects with control methods: Timers are objects with control methods:
```python ```python
t = mcrfpy.Timer("update", callback, 100) # callback(timer, runtime_ms) t = mcrfpy.Timer("update", callback, 100) # callback(timer, runtime_ms)
t.pause() t.pause()
t.resume() t.resume()
t.stop() t.stop()
t.restart() t.restart()
# Properties: name, interval, callback, active, paused, stopped, remaining, once # Properties: name, interval, callback, active, paused, stopped, remaining, once
``` ```
### Input Enums ### Input Enums
```python ```python
mcrfpy.Key.W # Keyboard keys mcrfpy.Key.W # Keyboard keys
mcrfpy.MouseButton.LEFT # Mouse buttons mcrfpy.MouseButton.LEFT # Mouse buttons
mcrfpy.InputState.PRESSED # Input states (PRESSED, RELEASED, HOLD) mcrfpy.InputState.PRESSED # Input states (PRESSED, RELEASED, HOLD)
mcrfpy.Easing.EASE_IN_OUT # Animation easing functions mcrfpy.Easing.EASE_IN_OUT # Animation easing functions
``` ```
## Current Issues & Limitations ## Current Issues & Limitations
**Consistency:** **Consistency:**
- [#126](../issues/126): Automated generation for perfect consistency - [#126](../issues/126): Automated generation for perfect consistency
- [#109](../issues/109): Vector lacks `[0]`, `[1]` indexing - [#109](../issues/109): Vector lacks `[0]`, `[1]` indexing
**Type Preservation:** **Type Preservation:**
- Collections can lose Python derived types - Collections can lose Python derived types
- Workaround: `RET_PY_INSTANCE` macro - Workaround: `RET_PY_INSTANCE` macro
## Design Decisions ## Design Decisions
**Why Python C API vs pybind11/SWIG?** **Why Python C API vs pybind11/SWIG?**
- Fine-grained control over type system - Fine-grained control over type system
- Direct integration with CPython internals - Direct integration with CPython internals
- No third-party dependencies - No third-party dependencies
- Zero-overhead abstraction - Zero-overhead abstraction
**Tradeoffs:** **Tradeoffs:**
- More verbose than pybind11 - More verbose than pybind11
- Manual memory management required - Manual memory management required
- But: Full control, no "magic" - But: Full control, no "magic"
--- ---
**Next Steps:** **Next Steps:**
- Review [[Adding-Python-Bindings]] for the step-by-step workflow - Review [[Adding-Python-Bindings]] for the step-by-step workflow
- See `docs/api_reference_dynamic.html` for the generated API reference - See `docs/api_reference_dynamic.html` for the generated API reference