Port espejo · C → Rust
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.
01 · El mecanismo
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.
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
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.
03 · Diseño de módulos
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.
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ódulo | Líneas | Responsabilidad |
|---|---|---|
| wm.rs | 2 152 | ciclo de vida de clientes, foco, ratón, tags |
| layouts.rs | 1 485 | los 13 layouts y los gaps |
| bar.rs | 1 158 | barra, pestañas, systray, previsualizaciones |
| core.rs | 1 127 | arranque, bucle, monitores, EWMH |
| drw.rs | 896 | dibujo, Xft, FFI de fontconfig a mano |
| events.rs | 766 | los 15 manejadores y el despacho |
| state.rs | 504 | globals, tipos y macros como fns inline |
| config.rs | 474 | toda la configuración de compilación |
| imlib2.rs | 97 | FFI de Imlib2 escrito a mano |
| main.rs | 20 | declaraciones de módulo y allows del crate |
04 · Verificación
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.
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
Un port espejo se juzga por fidelidad, no por elegancia. Estas cuatro decisiones parecen deuda técnica y son requisitos:
overflow-checks, así que una operación c_uint verificada
aborta el proceso donde el unsigned int de C envuelve en silencio.
Para un window manager, abortar significa cerrar la sesión. Todo lo portado usa
wrapping_add / wrapping_sub.
c_int. El C usa int
como booleano y compara contra cero en sitios donde el valor también se usa como número.
allow a nivel de crate silencian
non_snake_case para que un diff contra el original siga siendo
legible.
config::layouts termina en
{ null(), None }, porque cyclelayout y el indexado
LENGTH(layouts)-2 dependen de ella.
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».