2 puntos por GN⁺ 2024-02-17 | 2 comentarios | Compartir por WhatsApp
  • Aunque no se pueda ofrecer funcionalidad real por una plataforma específica o porque algo fue retirado, para evitar que las apps existentes se rompan hay que diseñar un comportamiento de “no hacer nada” dentro del contrato de API documentado
  • En entornos sin infraestructura de impresión, como Xbox, una respuesta exitosa con 0 impresoras es más segura para apps escritas con PC en mente que lanzar NotSupportedException
  • Este enfoque se conoce como API inert, un diseño que mantiene la superficie y la especificación de la API, pero evita que realice trabajo útil en la práctica
  • Si en una API de widgets en retirada CreateWidget devuelve éxito pero retorna nullptr, el llamador queda en un estado contradictorio; por eso, la ruta de error documentada con ERROR_CANCELLED es más consistente
  • Una API inert bien diseñada hace desaparecer solo la funcionalidad y usa las rutas de error que el código existente ya sabe manejar para reducir excepciones, handles inválidos y crashes

Aunque la funcionalidad desaparezca, el contrato de la API sigue ahí

  • A veces hay que hacer que una API no haga nada, pero incluso esa inacción debe ajustarse al código existente y al comportamiento documentado
  • Una inacción mal diseñada puede generar excepciones, valores de retorno contradictorios y estados inválidos, lo que termina en crashes de apps o errores innecesarios para el usuario

Cómo desactivar la API de impresión en Xbox

  • Windows cuenta con una amplia infraestructura de impresión, pero Xbox no dispone de ella
  • Si una app intenta imprimir en Xbox y la función de impresión lanza NotSupportedException, el problema puede agravarse
    • Es probable que las apps instaladas en Xbox se hayan probado principalmente en PC
    • En PC, imprimir siempre es posible, así que una excepción no controlada puede terminar en un crash de la app
    • Incluso si se captura la excepción, el usuario podría ver mensajes de error como “proporcione un incident code al equipo de soporte”
  • Un mejor diseño es que la función de impresión tenga éxito, pero informe que no hay impresoras instaladas
    • La app muestra la UI de selección de impresora y presenta una lista vacía
    • El usuario ve que no hay impresoras y puede cancelar la solicitud de impresión
  • Si una app intenta ayudar a instalar una impresora, la función de instalación puede regresar de inmediato con un código de resultado que indique que el usuario canceló la operación

Condiciones de una API inert

  • A este tipo de comportamiento de “no hacer nada” se le llama inert
  • Una API inert mantiene la superficie de la API y se comporta según la especificación, pero en la práctica no realiza ningún trabajo útil
  • Lo clave es no hacer nada de una forma consistente con la documentación y que minimice la posibilidad de romper el código existente
  • En una API de impresión, también podría añadirse una función para comprobar si realmente se puede imprimir
    • La app puede usar esa función para ocultar el botón Print en sistemas que no soportan impresión en absoluto
    • Las apps que simplemente asumen que se puede imprimir seguirán comportándose como en un sistema “sin impresoras y donde intentar instalarlas no tiene efecto”

Diseño que debe evitarse en una API de widgets en retirada

  • La API que se va a retirar tiene una función para crear un handle de widget, funciones que reciben ese handle y una función para cerrarlo
  • La propuesta inicial era que CreateWidget devolviera S_OK mientras establecía *widget = nullptr
    • La llamada tendría éxito, pero sin entregar un handle válido
    • En código de prueba real había casos que determinaban el éxito revisando si el handle era null, no el valor de retorno
  • Un diseño donde EnableWidget devolviera “handle inválido” también confunde al llamador
    • La app pasa a EnableWidget el handle recibido tras el éxito de CreateWidget
    • La API responde que ese handle no es válido
    • El llamador queda en un estado contradictorio: “pedí un widget, me lo dieron y luego me dicen que no era un widget”

Diseño corregido con una ruta de error documentada

  • La documentación existente ya incluye el valor de retorno ERROR_CANCELLED, que significa que el usuario canceló la creación del widget
  • Como la app ya debe manejar la posibilidad de que el widget no se cree por condiciones externas, puede hacerse que la creación del widget siempre falle como cancelación del usuario
  • El flujo del diseño corregido es simple
    • CreateWidget deja *widget = nullptr y devuelve HRESULT_FROM_WIN32(ERROR_CANCELLED)
    • Como la creación del widget siempre falla, no existe ningún handle de widget válido
    • GetWidgetAliases, EnableWidget y Close devuelven E_HANDLE, indicando que el handle no es válido
  • Con este enfoque tampoco hace falta crear un alias ficticio
    • Como no existe un widget válido, tampoco hay un caso en el que la app pueda pedir un alias de forma legítima

Criterio al llevar APIs de escritorio a otros entornos

  • En escritorio, la API de impresión siempre ha existido y la función para “obtener la lista de impresoras” está documentada para no lanzar excepciones
  • Si se lleva esta API a Xbox y se quiere que las apps de escritorio existentes sigan funcionando, el comportamiento inert correcto es devolver honestamente que “Xbox no tiene impresoras”
  • Lanzar una excepción ante la pregunta “¿cuántas impresoras hay?” no respeta el contrato de API documentado
  • El mismo principio se aplica al retirar una API existente
    • Aunque ya no haga trabajo útil, el comportamiento de la API debe seguir siendo consistente con su contrato
    • Es importante usar valores de retorno y estados que el código existente ya pueda manejar

2 comentarios

 
GN⁺ 2024-02-17
Opiniones en Lobste.rs
  • Lo que se dice aquí es que las funciones de impresión se comportan de forma consistente como si la impresión estuviera totalmente soportada, pero curiosamente nunca habrá una impresora real para imprimir, y eso explica muchas cosas
    Bromas aparte, no estoy de acuerdo con este tipo de programación excesivamente defensiva ni con esa experiencia de usuario. Así, el software deja de cumplir su función sin que nadie sepa por qué, y además no hay forma de averiguarlo. La app debería capturar el error y, si es posible, mostrar un mensaje amigable para el usuario, o al menos enseñarle el mensaje de error original. Si es una tarea en segundo plano, debería haber un registro de errores
    Admito que este texto está escrito desde la perspectiva de un desarrollador de API, no de un desarrollador de aplicaciones. Así que hay que documentar los errores de la API y ofrecer mensajes de error sobre los que quien la invoca pueda actuar
    Tampoco me gusta que se oculten botones en la UI solo porque no se tienen permisos de acceso. Si el espacio lo permite, me parece mejor mostrar el botón pero desactivado, y cuando el usuario pase el mouse por encima, mostrar un mensaje que explique cómo puede habilitarlo
    • También hay que considerar que este texto no está escrito desde la perspectiva de un simple desarrollador de API, sino de un desarrollador de la API de Windows. La postura histórica de Microsoft es que la API de Windows no debe romper compatibilidad aunque cambie la versión
    • Esta es una discusión típica sobre la ley de Postel / principio de robustez. A estas alturas todos sabemos que el principio de robustez ya envejeció mal y que dio origen a monstruos como HTML o enormes protocolos y formatos de archivo en los que es difícil escribir analizadores correctos
      En general, es mejor exigir corrección. Pero si ya tienes mil millones de usuarios existentes, es muy sensato evitar romper las cosas tanto como sea posible, y desde la perspectiva del usuario eso genera un valor real a nivel de sistema porque simplemente funciona. Al final, la actitud debería ser fallar rápido, pero no provocar fallos masivos
    • En este caso, probablemente uno de los API existentes no tiene errores documentados, así que es posible que desde el inicio no haya manejo de errores. Si no quieres romper todo el software existente, tienes que decir una mentira piadosa
      Esto se parece más a estabilidad de ABI que de API. En Windows, incluso el software compilado hace 15 años debe seguir funcionando en un sistema operativo nuevo siempre que sea posible. Como no puedes cambiar la firma de la función, si quieres que incluso una API que ya no tiene sentido siga funcionando, tienes que recurrir a mentiras piadosas
      Por ejemplo, la API todavía finge que Active Desktop existe. La alternativa sería romper en masa software heredado antiguo
    • Totalmente de acuerdo. Hay pocas cosas más frustrantes que perder tiempo buscando un botón que por alguna razón no aparece en mi pantalla, pero sí en la explicación de alguien al lado o en un tutorial
      Uno ya no sabe si la función desapareció o si quedó escondida en otra pantalla mientras tanto
    • Sí, la app debería capturar el error y mostrárselo al usuario.
      Pero si la app no lo hace, la gente que usa esa app culpa a Windows. Culpan a Windows, no a la app, e incluso pasa lo mismo cuando la app se cae
      Por eso Microsoft crea soluciones alternativas. Es mucho más fácil dejar que el trabajo de impresión simplemente desaparezca, y así el usuario se queda pensando un momento y luego lo acepta con un “ah, claro, no había impresora”
  • Si una función de instalación de impresora puede devolver el control de inmediato y retornar como código de resultado el usuario canceló la operación, desde el punto de vista del soporte de aplicaciones eso casi con certeza llevará a un comportamiento no deseado mucho más difícil de manejar que si la API de impresión lanzara una excepción desde el principio
  • Últimamente hago bastante programación asistida por IA, y veo que los agentes suelen meter con frecuencia verificaciones if para comprobar si algo es null. Cada vez que lo veo me acuerdo de este texto
    Así que les indiqué a los agentes que no repitan verificaciones de null, que usen funciones inocuas, y que validen una sola vez al momento de la declaración que el valor nunca será null
 
GN⁺ 2024-02-17
Opiniones en Hacker News
  • Lo aprendí como una forma de tragarse los errores, y lo considero una mala práctica.
    No solo no resuelve el problema real de que no se puede imprimir desde Xbox, sino que oculta qué tan roto está el software, lo que hace mucho más difícil encontrar bugs y probar.
    Me gusta que el panic de Go no esté pensado para abusarse en tiempo de ejecución ni para recuperarse de él, sino como una herramienta que hace sonar fuerte la sirena durante las pruebas para avisar que el programador se equivocó.
    Cuando los actores dentro de un sistema ocultan activamente sus propios defectos o errores, encontrar y corregir la causa se vuelve exponencialmente más difícil.

    • El diseño de errores depende del contexto.
      En los sistemas B2B, de microservicios y centrados en API con los que trabajo, lo correcto es hacer fallar rápido las solicitudes fuera de especificación.
      Pero Windows se ha esforzado muchísimo por mantener compatibilidad hacia atrás, e incluso hay casos en los que mantuvo vivo software famoso y roto como SimCity aplicando parches de memoria: https://arstechnica.com/gadgets/2022/10/windows-95-went-the-...
      En un entorno de escritorio donde el usuario no puede corregir el código del sistema, muchas veces la experiencia del usuario final importa más que la corrección.
      A Microsoft no le importaba tanto por qué ocurría un BSOD; aprendió que el software o hardware malo dañaba la reputación de Microsoft.
      Personalmente prefiero diseños que fallen rápido o se detengan ante entradas o condiciones inválidas, pero también entiendo el valor de este enfoque en ese entorno.
      Lo importante es el contexto, no aplicar dogmáticamente una u otra postura.
      Por ejemplo, no deberías llevar el enfoque de Chen al diseño de un reactor nuclear o de trading de alta frecuencia, pero puede ser un enfoque excelente para un motor de juegos.
    • Al diseñar un runtime alternativo para una plataforma nueva, lo clave es decidir por cuenta propia qué se considera un error.
      Decidir si dejar un componente del runtime como “inofensivamente inactivo” es una decisión de diseño anterior a si la implementación hará panic o no.
      En este caso, le corresponde a Microsoft definir qué significa “imprimir desde Xbox”.
      Puede definirlo como un error, o teóricamente como una función que podría soportarse más adelante pero que ahora no tiene una forma real de ejecutarse.
      Si se conecta una impresora USB y un “juego”, como por ejemplo un juego de Game Boy que soportaba la GB Printer mediante el puerto de expansión, sabe cómo imprimir, en teoría también podría imprimirse desde Xbox.
      Que un juego de Xbox intente imprimir no es necesariamente un error intrínseco; definirlo como error también es solo una elección con trade-offs, como la portabilidad de aplicaciones.
      Podrías mostrar una pantalla de selección sin impresoras seleccionables, como en Xbox; hacer que la API mienta diciendo que imprimió; o soportar la impresión real.
      Solo después de definir el significado de “imprimir desde Xbox” como un error, el hecho de que ese error no se lance y se trague pasa a ser un problema de implementación o depuración.
    • Esto no es tragarse un error; en términos de Linux, se parece más a no romper el espacio de usuario.
      Hay dos tratamientos posibles: como la máquina nunca tendrá impresora, devolver un error; o, como la máquina nunca tendrá impresora, devolver una lista vacía de impresoras.
      Ambos son válidos, pero solo uno no rompe el espacio de usuario.
    • Si lees el texto, habla más de la experiencia de usuario que de la experiencia del desarrollador.
      “Hacer sonar fuerte la sirena” es bueno para testers o desarrolladores, pero no tanto para el usuario final.
      Desde la perspectiva del usuario final, que se trague el error es mejor que que la app se cierre.
    • Me molesta que, desde hace décadas, cuando falla la conexión a Internet, simplemente aparezca un mensaje diciendo que falló.
      No se ha avanzado nada en saber qué falló: si fue el software de mi computadora, el hardware, el cable Ethernet o el Wi-Fi, el switch, el router, el módem de cable, el cable de Internet que llega a mi casa, el ISP o el sistema remoto al que intento conectarme.
  • Es previsible que haya reacciones negativas, pero este tipo de cosas sucias es precisamente la razón por la que un documento de Word ’97 o un juego compilado para MS-DOS hace 30 años se abre como esperabas cuando le das clic.
    La compatibilidad hacia atrás siempre es messy, y solo hay dos opciones: hacerla de forma imperfecta o no hacerla en absoluto.

    • Escucho mucho ese estribillo sobre Microsoft, pero con los juegos no tuve tanta suerte.
      Al final termino usando GOG, y GOG básicamente virtualiza el entorno.
      Por ejemplo, no creo que nadie logre instalar el Sim City original en Windows 11 con éxito.
    • Un archivo creado en Word ’97, incluso hace 25 años, no se abría exactamente como se esperaba en dos computadoras distintas.
      El formato dependía del driver de impresora instalado.
      En cambio, un archivo LaTeX de hace 35 años sigue funcionando bien hoy.
    • Incluso con un archivo de 1997 hay que tener suerte, y es muy probable que simplemente se vea mal al abrirlo en cualquier app de oficina.
      Word no podía garantizar que el formato no cambiara al volver a abrirlo en la misma computadora y con la misma versión, y por eso aprendí TeX.
      En juegos, Microsoft también rompió muchísimos títulos cuando cerró GFWL.
      El Dark Souls original no se podía jugar hasta que fue relanzado en Steam, así que había que usar una copia pirata.
    • En Windows moderno de 64 bits, el software de MS-DOS no funciona.
      Aparece un cuadro de diálogo de “no compatible”, y puedes descargar dosbox, pero eso no es “exactamente como esperabas”.
    • Me gustaría que eso fuera cierto, pero sufrí muchísimo tratando de ejecutar juegos que había guardado desde mi infancia.
      Casi siempre me rendí o los volví a comprar en GOG; en la práctica, terminé pagando por el trabajo de haber resuelto los problemas de compatibilidad.
  • No hay nada tan frustrante como una UI de dispositivos que da a entender que podrían existir, pero luego dice que no hay ninguno en este momento
    Al final uno tiene que dedicar tiempo para descubrir que esos dispositivos no son compatibles y que esa pantalla no era más que un mockup que alguien hizo

    • Aun así, probablemente sea menos frustrante que que la app simplemente se muera
      Si la app no contempló el caso de que la impresión no esté soportada y la salida lanza una excepción, probablemente la app se muera tal cual
      Una app que sí contempló que la impresión no esté soportada debería comprobar primero con una API explícita si la impresión está soportada y, si no lo está, no mostrar la UI de impresión
      El artículo no trata sobre esas apps, sino sobre cómo manejar apps que no lo comprueban antes
      El bug está en la app, pero una buena plataforma debería intentar que incluso las malas apps funcionen lo más posible, en vez de simplemente dejarlas morir
    • Aquí quizá la responsabilidad recaiga más en el autor del software que no manejó la situación de la impresora, no en Microsoft
      El papel de Microsoft es evitar que la app se muera solo porque el proveedor del software no tuvo en cuenta las impresoras
  • Que cuando una app intenta imprimir se le pida al usuario elegir una impresora y se le muestre una lista vacía... parece que Microsoft no aprendió nada en 30 años: https://www.reddit.com/r/hacking/comments/djvzd/windows_nt_l...

  • La dirección concreta que propone el artículo —que los componentes deben sufrir antes que los usuarios— es correcta, pero el encuadre resulta muy molesto
    Cosas como “a veces hay que hacer que una API no haga nada” o “está mal que una función de impresión lance NotSupportedException” de ninguna manera son consejos generales
    Esto es un hack para dar soporte a clientes pésimos y, aunque a veces sea necesario, no es un consejo normal ni generalizable
    Ese tipo de formulaciones deja ver huellas de haber interiorizado el sufrimiento de ser desarrollador de Microsoft

    • No es un cliente pésimo; es un cliente escrito antes de que decidieras hacer este cambio
      Si el cliente no cambió y la API lo rompe, lo pésimo no es el cliente, sino la API
    • Si una plataforma de software fue adoptada por más que unos pocos desarrolladores, inevitablemente habrá apps pésimas, y entonces hay que hacer este tipo de cosas
      En cualquier plataforma ampliamente adoptada que se tome en serio la compatibilidad hacia atrás, esto es completamente normal y generalizable
      Windows es el caso representativo, pero pasa lo mismo con la ABI del kernel de Linux, glibc, la plataforma web, Java, etc.
    • Hay veces en que hay que implementar una API que los clientes existentes ya están usando
      No se puede volver el tiempo atrás para recompilar o reescribir los clientes
      Si el cliente ya está comprobando NotSupportedException, entonces hay que devolver eso; pero si no, hace falta otro enfoque
    • Si estuvieras en el equipo de Windows y mostraras esa actitud, si yo fuera gerente quizá te despediría
      Esto es Windows
      No se deben romper las aplicaciones de las que dependen los usuarios
      Si hay apps cliente “pésimas” o que se comportan mal, hay que usar todos los workarounds, shims y hacks de compatibilidad necesarios para que se comporten como se espera
      Incluso si eso implica meter código especial de gestión de memoria para que SimCity funcione
    • Ese “cliente pésimo” puede haber sido escrito por una empresa que cerró hace mucho
      Tu cliente depende de ese cliente, y si deja de funcionar por algo que tú cambiaste, te va a culpar a ti
  • Aquí todos se obsesionan con “no hacer nada” y “manejo de errores”, pero parece que no se explicó bien que el artículo no propone tragarse todos los problemas y cantar kumbaya
    El contexto real es emulación y compatibilidad para software que no va a cambiar, y eso es muy distinto de la mayoría de las situaciones
    Microsoft mismo a menudo difumina la frontera entre ambas cosas, lo que confunde más, pero aquí no se habla del manejo de errores en general
    La emulación consiste, en esencia, en mentir
    En una Xbox no hay forma real de conectar una impresora, estás ejecutando juegos de Windows en Linux, y un iPhone falso puede ser en realidad un servidor ARM en AWS
    Si vas a mentir, tienes que hacerlo de forma convincente, para que las aplicaciones existentes sigan funcionando
    Cuando Microsoft dice que se pueden portar apps existentes de Windows a Xbox, las apps de Windows tenían la promesa de que podían imprimir, así que Xbox tiene que mentir para que eso parezca posible
    En cambio, si un programa está haciendo un use-after-free y se lo esquiva en silencio fingiendo que no hay bug, eso no es un manejo correcto
    Porque nadie prometió que hasta los use-after-free existentes serían compatibles bug por bug en la nueva plataforma

    • En ninguna parte del artículo aparece el contexto de emular para software que no va a cambiar
      Solo asumiste esa interpretación porque respalda la postura que querías defender
      De hecho, lo que dice el artículo se aplica igual si mañana escribo un programa para Xbox e intento imprimir
      No hay nada que distinga que este comportamiento solo aplica a software viejo y sin mantenimiento
  • Este enfoque me gusta y no me gusta a la vez
    Instintivamente no me gusta tratar los problemas mediante cumplimiento malicioso
    Por otro lado, si el objetivo es permitir que más usuarios ejecuten más software en la plataforma, aunque la impresión esté rota, estoy totalmente de acuerdo en que es una buena decisión

  • Este hilo de comentarios fue raro
    Claramente hay gente a la que le disgusta cualquier cosa que haga Microsoft
    El punto central del blog es hacer que llevar apps (UWP) a XBOX sea un proceso sin fricciones
    Que la app “simplemente funcione” sin recompilar, sin ifdef sucios y sin trabajo adicional, y luego el desarrollador ya pueda ajustarla a la plataforma XBOX
    Lo importante es que el desarrollador pudo llevar la app a XBOX sin ningún esfuerzo, y eso es bueno tanto para la plataforma como para el desarrollador
    Para el usuario, que la lista de impresoras esté vacía es una señal clara de que no puede imprimir
    Sobre todo, la app no se muere ni muestra el peor tipo de error, como “Ocurrió un problema. Inténtalo de nuevo más tarde”
    Ese mensaje no es una mejor experiencia de usuario
    El desarrollador puede recompilar la app y poner un mensaje más explicativo, pero entonces volvemos al punto de partida

    • Parece que el 95% de este hilo no entendió el artículo
      Por ejemplo, la acusación de que se tragan los errores no viene al caso en absoluto
  • El punto clave que pasan por alto quienes critican este artículo es que aquí no hay una situación excepcional en la que haya que lanzar una excepción.
    No hay una impresora conectada a este dispositivo y, en este caso, por definición es así, por lo que la app debería manejar esa situación limpiamente, no morirse.
    Una laptop no lanzaría una excepción solo porque no puede acceder a una impresora.

    • Si quieren un ejemplo moderno de un producto no Microsoft ampliamente usado que maneja mal la disponibilidad de impresoras, está gnome-control-center.
      Si agregas una impresora en Configuración y luego, durante la instalación, pasas a la pestaña “Color Profiles”, con el timing adecuado gnome-control-center se muere.
      Si lo miras de cerca, intentó enumerar las impresoras disponibles; sabe que debería haber una impresora, pero como la información aún no existe, simplemente se muere.
      Por suerte, basta con esperar unos segundos a que GNOME termine de instalar la impresora y volver a abrir Configuración.
      Aun así, si se trata de una app clave que ayuda a usuarios finales de todo tipo de contextos a usar bien el escritorio, en un mundo ideal nunca debería morirse.
      Pensar en las condiciones límite es difícil, y es todavía más difícil imaginar que el propio manejo de condiciones límite también tiene otras condiciones límite.
      Con razón o sin ella, al menos el autor está pensando con más profundidad que “es una mala app, hay que reescribirla para que soporte Wayland”.
  • Hubo una época en que se consideraba una gran estrategia que los navegadores hicieran su mejor esfuerzo por mostrar una página aunque el código HTML tuviera errores.
    La idea era adivinar lo mejor posible la intención del autor y seguir adelante, porque los errores son malos y los usuarios no quieren errores.
    Creía que habíamos aprendido de esa experiencia, pero parece que no.

    • Los navegadores siguen usando esa estrategia.
      El intento de XHTML de reemplazarla con validación estricta fracasó; en cambio, HTML5, que sí tuvo éxito, volvió atrás y definió explícitamente cómo interpretar todas las secuencias inválidas posibles, para que al menos los navegadores se comportaran de manera consistente.
    • Que los navegadores fueran tolerantes con la entrada hizo posible la web moderna.
      Si los primeros navegadores hubieran mostrado un error apenas se encontraban con una etiqueta desconocida, las nuevas funciones de los navegadores se habrían asfixiado al nacer.
    • Si ingresas Hello y World!, puedes comprobar si, gracias a la sabiduría que adquirimos, ahora aparece un error.
      Pero si todavía se renderiza bien, quizá significa que aún tenemos mucho que aprender, o que tiene sentido manejar con elegancia ciertos tipos de errores y morirse ante situaciones excepcionales.
    • En los últimos 20 años, el avance del software no ha sido gran cosa salvo por los LLM, y los LLM también fueron arruinados por la censura.
      Siento que si viajara en el tiempo a 1999 no extrañaría demasiado.
    • Me pregunto qué creemos que aprendimos.