adding python bindings base

This commit is contained in:
Kbz-8
2025-10-15 22:32:21 +02:00
parent d6a7450bdd
commit 73d31c196e
3 changed files with 294 additions and 0 deletions

2
.gitignore vendored
View File

@@ -23,3 +23,5 @@
objs/ objs/
build/ build/
example/Test example/Test
macrolibpy/__pycache__
example/__pycache__

18
example/main.py git.filemode.normal_file
View File

@@ -0,0 +1,18 @@
import macrolibpy as mlpy
if __name__ == '__main__':
mlx = mlpy.Context.create()
win = mlx.new_window(800, 600, "MacroLibX window")
def onevent(ev):
if ev == 0:
mlx.loop_end()
def onupdate():
for x in range(0, 50):
for y in range(0, 50):
win.pixel_put(100 + x, 100 + y, 0xff0000ff)
win.on_event(mlpy.EventType.WINDOW_EVENT, onevent)
mlx.add_loop_hook(onupdate)
mlx.loop()

274
macrolibpy/__init__.py git.filemode.normal_file
View File

@@ -0,0 +1,274 @@
"""
macrolibpy — thin, Pythonic bindings over MacroLibX.
"""
from __future__ import annotations
from enum import IntEnum
from typing import Callable, Optional, Any, List
import os, sys
from cffi import FFI
import pathlib
__all__ = ["Context", "Window", "Image", "Event"]
ffi = FFI()
# NOTE: This is a curated declaration set focused on the core API
ffi.cdef(
r"""
typedef unsigned char uint8_t;
typedef unsigned int uint32_t;
typedef void (*mlx_function)(void);
typedef struct mlx_context_handler* mlx_context;
typedef struct mlx_window_handler* mlx_window;
typedef struct mlx_image_handler* mlx_image;
typedef struct mlx_color
{
uint32_t rgba;
} mlx_color;
typedef enum mlx_event_type
{
MLX_KEYDOWN = 0,
MLX_KEYUP = 1,
MLX_MOUSEDOWN = 2,
MLX_MOUSEUP = 3,
MLX_MOUSEWHEEL = 4,
MLX_WINDOW_EVENT = 5
} mlx_event_type;
typedef struct mlx_window_create_info
{
mlx_image render_target;
const char* title;
int width;
int height;
_Bool is_fullscreen;
_Bool is_resizable;
} mlx_window_create_info;
mlx_context mlx_init();
void mlx_set_fps_goal(mlx_context mlx, int fps);
void mlx_destroy_context(mlx_context mlx);
mlx_window mlx_new_window(mlx_context mlx, const mlx_window_create_info* info);
void mlx_destroy_window(mlx_context mlx, mlx_window win);
void mlx_set_window_position(mlx_context mlx, mlx_window win, int x, int y);
void mlx_set_window_size(mlx_context mlx, mlx_window win, int width, int height);
void mlx_set_window_title(mlx_context mlx, mlx_window win, const char* title);
void mlx_set_window_fullscreen(mlx_context mlx, mlx_window win, _Bool enable);
void mlx_get_window_position(mlx_context mlx, mlx_window win, int* x, int* y);
void mlx_get_window_size(mlx_context mlx, mlx_window win, int* x, int* y);
void mlx_clear_window(mlx_context mlx, mlx_window win, mlx_color color);
void mlx_get_screen_size(mlx_context mlx, mlx_window win, int* w, int* h);
void mlx_add_loop_hook(mlx_context mlx, void(*f)(void*), void* param);
void mlx_loop(mlx_context mlx);
void mlx_loop_end(mlx_context mlx);
void mlx_mouse_show(mlx_context mlx);
void mlx_mouse_hide(mlx_context mlx);
void mlx_mouse_move(mlx_context mlx, mlx_window win, int x, int y);
void mlx_mouse_get_pos(mlx_context mlx, int* x, int* y);
void mlx_on_event(mlx_context mlx, mlx_window win, mlx_event_type event, void(*f)(int, void*), void* param);
void mlx_pixel_put(mlx_context mlx, mlx_window win, int x, int y, mlx_color color);
mlx_image mlx_new_image(mlx_context mlx, int width, int height);
mlx_image mlx_new_image_from_file(mlx_context mlx, char* filename, int* width, int* height);
void mlx_destroy_image(mlx_context mlx, mlx_image image);
mlx_color mlx_get_image_pixel(mlx_context mlx, mlx_image image, int x, int y);
void mlx_set_image_pixel(mlx_context mlx, mlx_image image, int x, int y, mlx_color color);
void mlx_put_image_to_window(mlx_context mlx, mlx_window win, mlx_image image, int x, int y);
void mlx_string_put(mlx_context mlx, mlx_window win, int x, int y, mlx_color color, char* str);
void mlx_set_font(mlx_context mlx, char* filepath);
void mlx_set_font_scale(mlx_context mlx, char* filepath, float scale);
"""
)
def _candidateLibNames() -> List[str]:
if sys.platform.startswith("linux"):
return ["libmlx.so"]
if sys.platform == "darwin":
return ["libmlx.dylib"]
if sys.platform.startswith("win"):
# depending on build system, either of these may exist
return ["mlx.dll", "libmlx.dll"]
return []
def _candidateSdl2Names() -> List[str]:
if sys.platform.startswith("linux"):
# Try common SONAMEs (distro dependent)
return ["libSDL2-2.0.so.0", "libSDL2.so.0", "libSDL2.so"]
if sys.platform == "darwin":
return ["libSDL2-2.0.0.dylib", "libSDL2.dylib"]
if sys.platform.startswith("win"):
return ["SDL2.dll"]
return []
def _loadLibrary(candidates: List[str]):
last_err: Optional[BaseException] = None
RTLD_NOW = 2
RTLD_GLOBAL = 0x100
for path in candidates:
try:
return ffi.dlopen(path, RTLD_NOW | RTLD_GLOBAL)
except OSError as e:
last_err = e
raise OSError(
f"Could not load library:\n"
f"\tTried: {candidates}.\n"
f"\tLast error: {last_err}"
)
def _dlopenMlx():
_loadLibrary(_candidateSdl2Names())
candidates: List[str] = []
for name in _candidateLibNames():
candidates.append(os.path.join(pathlib.Path(__file__).parent.parent.resolve(), name))
return _loadLibrary(candidates)
lib = _dlopenMlx()
class EventType(IntEnum):
KEYDOWN = 0
KEYUP = 1
MOUSEDOWN = 2
MOUSEUP = 3
MOUSEWHEEL = 4
WINDOW_EVENT = 5
class MlxError(RuntimeError):
pass
# Keep Python references to cffi callbacks alive
_alive_callbacks: List[Any] = []
def _makeLoopCallback(pyfunc: Callable[[], None]):
@ffi.callback("void(void*)")
def cfunc(userdata):
pyfunc()
_alive_callbacks.append(cfunc)
return cfunc
def _makeEventCallback(pyfunc: Callable[[int], None]):
@ffi.callback("void(int, void*)")
def cfunc(event, userdata):
pyfunc(int(event))
_alive_callbacks.append(cfunc)
return cfunc
def _rgbaToColor(rgba: int):
c = ffi.new("mlx_color*")
c[0].rgba = int(rgba) & 0xFFFFFFFF
return c[0]
class Context:
__slots__ = ("_ctx",)
def __init__(self, _ptr) -> None:
self._ctx = _ptr
@classmethod
def create(cls) -> "Context":
ctx = lib.mlx_init()
if ctx == ffi.NULL:
raise MlxError("mlx_init failed")
return cls(ctx)
def set_fps_goal(self, fps: int) -> None:
lib.mlx_set_fps_goal(self._ctx, int(fps))
def add_loop_hook(self, fn: Callable[[], None]) -> None:
lib.mlx_add_loop_hook(self._ctx, _makeLoopCallback(fn), ffi.NULL)
def new_window(
self,
width: int,
height: int,
title: str,
resizable: bool = True,
fullscreen: bool = False,
) -> "Window":
info = ffi.new("mlx_window_create_info*")
info.render_target = ffi.NULL
info.title = ffi.from_buffer(title.encode("utf-8"))
info.width = int(width)
info.height = int(height)
info.is_fullscreen = bool(fullscreen)
info.is_resizable = bool(resizable)
win = lib.mlx_new_window(self._ctx, info)
if win == ffi.NULL:
raise MlxError("mlx_new_window failed")
return Window(self, win)
def new_image(self, width: int, height: int) -> "Image":
img = lib.mlx_new_image(self._ctx, int(width), int(height))
if img == ffi.NULL:
raise MlxError("mlx_new_image failed")
return Image(self, img)
def loop(self) -> None:
lib.mlx_loop(self._ctx)
def loop_end(self) -> None:
lib.mlx_loop_end(self._ctx)
def destroy(self) -> None:
if getattr(self, "_ctx", None):
lib.mlx_destroy_context(self._ctx)
self._ctx = ffi.NULL
def __del__(self):
try:
self.destroy()
except Exception:
pass
class Window:
__slots__ = ("_ctx", "_win")
def __init__(self, ctx: Context, win) -> None:
self._ctx = ctx
self._win = win
def set_title(self, title: str) -> None:
lib.mlx_set_window_title(self._ctx._ctx, self._win, ffi.from_buffer(title.encode("utf-8")))
def clear(self, rgba: int) -> None:
lib.mlx_clear_window(self._ctx._ctx, self._win, _rgbaToColor(rgba))
def pixel_put(self, x: int, y: int, rgba: int) -> None:
lib.mlx_pixel_put(self._ctx._ctx, self._win, int(x), int(y), _rgbaToColor(rgba))
def put_image(self, image: "Image", x: int = 0, y: int = 0) -> None:
lib.mlx_put_image_to_window(self._ctx._ctx, self._win, image._img, int(x), int(y))
def on_event(self, event_type: EventType | int, fn: Callable[[int], None]) -> None:
lib.mlx_on_event(self._ctx._ctx, self._win, int(event_type), _makeEventCallback(fn), ffi.NULL)
def destroy(self) -> None:
if getattr(self, "_win", None):
lib.mlx_destroy_window(self._ctx._ctx, self._win)
self._win = ffi.NULL
def __del__(self):
try:
self.destroy()
except Exception:
pass
class Image:
__slots__ = ("_ctx", "_img")
def __init__(self, ctx: Context, img) -> None:
self._ctx = ctx
self._img = img
def destroy(self) -> None:
if getattr(self, "_img", None):
lib.mlx_destroy_image(self._ctx._ctx, self._img)
self._img = ffi.NULL
def __del__(self):
try:
self.destroy()
except Exception:
pass