3 Python Binding Layer
John McCardle edited this page 2026-02-07 23:49:47 +00:00

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.

Quick Reference

Related Issues:

  • #126 - Generate Perfectly Consistent Python Interface
  • #109 - Vector Convenience Methods
  • #92 - Inline C++ Documentation System (Closed - Implemented)

Key Files:

  • 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/PyObjectUtils.h - Utility functions for Python/C++ conversion
  • src/UIDrawable.h - RET_PY_INSTANCE macro pattern
  • Individual class binding files: src/Py*.cpp

Reference Documentation:

Architecture Overview

Module Structure

mcrfpy (C extension module)
|-- Types
|   |-- UI: Frame, Caption, Sprite, Grid, Entity
|   |-- Geometry: Arc, Circle, Line
|   |-- Grid Layers: TileLayer, ColorLayer
|   |-- Data: Color, Vector, Texture, Font
|   |-- Scene: Scene (with children, on_key)
|   |-- Timer: Timer (with stop, pause, resume)
|   |-- Pathfinding: AStarPath, DijkstraMap
|   |-- Enums: Key, MouseButton, InputState, Easing
|   +-- Tiled: TileSetFile, WangSet, LdtkProject, AutoRuleSet
|
|-- Module Functions
|   |-- current_scene (property)
|   |-- step(dt)
|   |-- start_benchmark(), end_benchmark(), log_benchmark()
|   +-- find() (scene lookup)
|
+-- Submodules
    +-- automation (screenshots, mouse, keyboard)

Entry Point: src/McRFPy_API.cpp::PyInit_mcrfpy()

Binding Patterns

Pattern 1: PyGetSetDef for Properties

Properties exposed via getter/setter arrays:

PyGetSetDef UISprite::getsetters[] = {
    {"x", (getter)Drawable::get_member, (setter)Drawable::set_member,
     MCRF_PROPERTY(x, "X coordinate of the sprite."),
     (void*)SPRITE_X},
    {"texture", (getter)UISprite::get_texture, (setter)UISprite::set_texture,
     MCRF_PROPERTY(texture, "Sprite texture reference."),
     NULL},
    {NULL}  // Sentinel
};

Pattern 2: PyMethodDef for Methods

Methods exposed via method definition arrays:

PyMethodDef UIGrid::methods[] = {
    {"at", (PyCFunction)UIGrid::at, METH_VARARGS | METH_KEYWORDS,
     MCRF_METHOD(Grid, at,
         MCRF_SIG("(x: int, y: int)", "GridPoint"),
         MCRF_DESC("Access grid cell at position."),
         MCRF_ARGS_START
         MCRF_ARG("x", "X coordinate")
         MCRF_ARG("y", "Y coordinate")
         MCRF_RETURNS("GridPoint object at that position")
     )},
    {NULL}
};

Pattern 3: RET_PY_INSTANCE Macro

Converting C++ objects to Python requires type-aware allocation:

RET_PY_INSTANCE(target);
// Expands to switch on target->derived_type():
//   - Allocates correct Python type (Frame, Caption, Sprite, Grid)
//   - Assigns shared_ptr to data member
//   - Returns PyObject*

File: src/UIDrawable.h

Documentation Macro System

Since October 2025, all Python-facing documentation uses macros from src/McRFPy_Doc.h:

#include "McRFPy_Doc.h"

// Method documentation
MCRF_METHOD(ClassName, method_name,
    MCRF_SIG("(arg: type)", "return_type"),
    MCRF_DESC("What the method does."),
    MCRF_ARGS_START
    MCRF_ARG("arg", "Argument description")
    MCRF_RETURNS("Return value description")
)

// Property documentation
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.

Common Patterns

Type Preservation in 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.

Constructor Keywords

All public types use keyword arguments:

# UI types
frame = mcrfpy.Frame(pos=(100, 200), size=(300, 150))
caption = mcrfpy.Caption(text="Hello", pos=(10, 10))
sprite = mcrfpy.Sprite(x=50, y=50, sprite_index=0)

# Grid types
grid = mcrfpy.Grid(grid_size=(50, 50), pos=(0, 0), size=(800, 600))
entity = mcrfpy.Entity(grid_pos=(10, 10), sprite_index=42)

# Data types
color = mcrfpy.Color(255, 128, 0, 200)
texture = mcrfpy.Texture("assets/sprites/tileset.png", 16, 16)

# Scene and Timer
scene = mcrfpy.Scene("my_scene")
timer = mcrfpy.Timer("my_timer", callback, 500)  # callback(timer, runtime)

PyArgHelpers

Standardized argument parsing for tuples vs separate args:

#include "PyArgHelpers.h"

// Accept both (x, y) and x, y formats
PyArgParseTuple_IntIntHelper(args, kwds, x, y, "position", "x", "y");

Key Subsystems

Scene System

Scenes are first-class Python objects:

scene = mcrfpy.Scene("game")
scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(100, 100)))
scene.on_key = lambda key, action: None  # Key enum, InputState enum
mcrfpy.current_scene = scene

Animation System

Animation is a method on UIDrawable objects:

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)
# callback receives (target, property_name, final_value)

Timer System

Timers are objects with control methods:

t = mcrfpy.Timer("update", callback, 100)  # callback(timer, runtime_ms)
t.pause()
t.resume()
t.stop()
t.restart()
# Properties: name, interval, callback, active, paused, stopped, remaining, once

Input Enums

mcrfpy.Key.W          # Keyboard keys
mcrfpy.MouseButton.LEFT   # Mouse buttons
mcrfpy.InputState.PRESSED  # Input states (PRESSED, RELEASED, HOLD)
mcrfpy.Easing.EASE_IN_OUT  # Animation easing functions

Current Issues & Limitations

Consistency:

  • #126: Automated generation for perfect consistency
  • #109: Vector lacks [0], [1] indexing

Type Preservation:

  • Collections can lose Python derived types
  • Workaround: RET_PY_INSTANCE macro

Design Decisions

Why Python C API vs pybind11/SWIG?

  • Fine-grained control over type system
  • Direct integration with CPython internals
  • No third-party dependencies
  • Zero-overhead abstraction

Tradeoffs:

  • More verbose than pybind11
  • Manual memory management required
  • But: Full control, no "magic"

Next Steps:

  • Review Adding-Python-Bindings for the step-by-step workflow
  • See docs/api_reference_dynamic.html for the generated API reference