Port espejo · C → Rust

Cómo funciona chadwm-rs

Un window manager de X11 no es una jerarquía de objetos: es un bucle de eventos colgando de tres tablas de punteros a función y un bloque de estado global mutable. Entender el port es entender esas cuatro cosas.

C portado
5 678
Rust
8 679
Módulos
10
Fotogramas idénticos
19/19

01 · El mecanismo

Todo pasa por una tabla que empieza vacía

El programa hace dos cosas: arranca y luego gira. En el arranque, setup() abre el display, reserva colores y cursores, y llama a init_handlers(). Después, run() se queda bloqueado en XNextEvent hasta que el servidor X le pasa un evento, mira el tipo, y salta a la función que haya en esa posición de la tabla.

Los handlers no dibujan nada por sí mismos. Modifican el estado global —qué monitor está seleccionado, qué cliente tiene el foco, la lista enlazada de ventanas— y luego llaman a arrange(), que recalcula geometrías y las empuja a Xlib. El servidor X aplica los cambios y, si eso genera nuevos eventos, el ciclo se repite.

ARRANQUE · UNA VEZ main() setup() scan() run() init_handlers() rellena 15 entradas si no se llama, la tabla queda en None y TODO evento se descarta en silencio BUCLE · POR CADA EVENTO servidor X XNextEvent handler[ev.type] KeyPress → keypress MapRequest → maprequest … → … events.rs el manejador wm.rs · bar.rs layouts.rs deciden qué cambia muta state.rs selmon · mons static mut arrange() → Xlib: XMoveResizeWindow · XSetInputFocus · drw_map el servidor aplica la geometría; si eso genera eventos nuevos, el bucle vuelve a empezar
La tabla handler[] es el único punto por el que entra el trabajo, y arranca entera en None. En C se inicializa con un designated initializer que Rust no puede replicar entre módulos, así que el port la rellena a mano desde setup(). Omitir esa llamada no da error de compilación: da un WM que arranca, dibuja la barra y luego ignora el teclado y el ratón para siempre.

02 · Indirección

Tres tablas de punteros a función, no una

La primera tabla despacha eventos. Pero dentro de keypress hay una segunda: recorre keys[] buscando el atajo que coincida y llama a su función. Y dentro de arrangemon hay una tercera: el layout activo es un puntero a función guardado en el monitor.

Por eso el port conserva punteros crudos en lugar de enum o Box<dyn Fn>: el C compara identidad de esos punteros —arrange == monocle— para decidir si mostrar la barra de pestañas o cómo arrastrar el divisor. Traducir eso a algo más idiomático rompería el comportamiento.

① EVENTOS handler[LASTEvent] indexada por ev.type unsafe fn(*mut XEvent) la llena events::init_handlers() la llama core::run() ② ATAJOS keys[N].func búsqueda lineal por keysym + mods unsafe fn(*const Arg) la define config.rs (const) la recorre events::keypress() ③ LAYOUT lt[sellt]->arrange guardado en cada Monitor unsafe fn(*mut Monitor) la define config.rs layouts[] la invoca wm::arrangemon() La consecuencia para el port El C no solo LLAMA a través de estos punteros: compara su identidad para tomar decisiones. if (m->lt[m->sellt]->arrange == monocle) → mostrar la barra de pestañas Rust avisa de que comparar punteros a función no es fiable. Los 10 warnings son intencionados: silenciarlos «arreglando» la comparación cambiaría el comportamiento. Se coercionan siempre desde el mismo sitio —layouts::monocle as LayoutFn— para que la identidad sobreviva.
Las tres tablas tienen firmas distintas y dueños distintos, pero el mismo patrón: una función anónima invocada por índice. Es lo que hace a dwm configurable en tiempo de compilación sin capas de abstracción — y lo que obliga al port a quedarse en punteros crudos.

03 · Diseño de módulos

Un archivo de 3 896 líneas repartido en cinco

El C vive casi entero en dwm.c, con los parches incluidos como #include de archivos .c en lugar de unidades de compilación separadas. El port lo corta por responsabilidad, con una regla: cada función mantiene su nombre en C, para que ambos árboles se puedan diferenciar línea a línea.

state.rs es distinto en especie. No es «los tipos»: es el estado global mutable que en C son static de ámbito de archivo. Todos los demás módulos lo leen y lo escriben. Esa es la forma real del programa.

FUENTE EN C MÓDULO EN RUST dwm.c 3 896 líneas manejadores de eventos ciclo de vida de clientes monitores · EWMH barra · pestañas · systray structs · globals · macros events.rs 766 wm.rs 2 152 core.rs 1 127 bar.rs 1 158 drw.c + util.c dibujo · fuentes Xft drw.rs 896 vanitygaps.c movestack.c shiftview.c layouts.rs 1 485 config.h + themes/onedark.h config.rs 474 sin contraparte en C (el enlazado -lImlib2) imlib2.rs 97 state.rs 504 dpy · root · drw mons · selmon handler[] scheme · cursor running static mut Client · Monitor Pertag · Layout ISVISIBLE() WIDTH() TEXTW() TAGMASK() los demás módulos lo leen y lo escriben no es «los tipos»: es el estado del programa entero
Los números son líneas de código reales. El reparto no sigue capas: sigue las responsabilidades que ya estaban implícitas dentro de dwm.c. Lo único que no existe en el original es imlib2.rs, declaraciones extern "C" escritas a mano porque ningún crate de Rust cubría Imlib2 — el C se limitaba a enlazar -lImlib2.
MóduloLíneasResponsabilidad
wm.rs2 152ciclo de vida de clientes, foco, ratón, tags
layouts.rs1 485los 13 layouts y los gaps
bar.rs1 158barra, pestañas, systray, previsualizaciones
core.rs1 127arranque, bucle, monitores, EWMH
drw.rs896dibujo, Xft, FFI de fontconfig a mano
events.rs766los 15 manejadores y el despacho
state.rs504globals, tipos y macros como fns inline
config.rs474toda la configuración de compilación
imlib2.rs97FFI de Imlib2 escrito a mano
main.rs20declaraciones de módulo y allows del crate

04 · Verificación

El binario en C es un oráculo ejecutable

Un port espejo tiene una ventaja que una reescritura no tiene: el original sigue ejecutándose. Cualquier duda de comportamiento se resuelve pasando ambos binarios por la misma sesión guionizada en dos servidores X anidados y comparando cada fotograma.

El paso que hace válida la comparación es el menos obvio: ejecutar el binario en C contra sí mismo primero. Dio 0 diferencias en los 19 fotogramas, lo que demuestra que el banco de pruebas es determinista. Sin esa línea base, una diferencia pequeña no significa nada.

una sola entrada 19 pasos de xdotool estado fijo · 3 clientes Xephyr :8 ./dwm · el original en C referencia Xephyr :9 chadwm-rs · el port perfil debug: overflow-checks import import 19 png 1280×800 19 png 1280×800 compare -metric AE 19 / 19 0 px de diferencia Línea base imprescindible: el mismo C contra sí mismo en dos sesiones → 0 / 19. El banco es determinista, así que una única diferencia es una divergencia real y no ruido de temporización.
La primera pasada dio 18/19. El fotograma discrepante —281 píxeles en los botones de la barra de pestañas— resultó ser un bug de verdad, no ruido, precisamente porque la línea base era 0.
El bug que encontró

En drw_text, el C calcula ty = y + (h - usedfont->h) / 2 + ascent. Como h y usedfont->h son unsigned int, las conversiones aritméticas usuales de C promueven también y y ascent: la expresión entera se evalúa sin signo y solo el guardado final en int ty desborda. La traducción la tenía con signo.

Y no es un caso raro. drawtab pasa h = th - vertpadbar, que ya vale ~2³² cuando la barra de pestañas está oculta — así que el C original lleva años apoyándose en desbordamiento de entero con signo, que es comportamiento indefinido, en cada redibujado de la barra. Se libra solo porque esa ventana tiene altura cero.

05 · Reglas del espejo

Lo que no se puede «mejorar»

Un port espejo se juzga por fidelidad, no por elegancia. Estas cuatro decisiones parecen deuda técnica y son requisitos:

Alcance honesto

19 de 19 fotogramas idénticos, en dev y en release, cubren layouts, gaps, foco, zoom, tags, barra, flotante, pantalla completa, el markup de estado y un cliente transient. No cubren multi-monitor, systray con una bandeja real, ni las previsualizaciones de tags. «Renderiza idéntico en 19 estados probados» no es «listo para producción».