2 pontos por GN⁺ 2024-02-17 | 2 comentários | Compartilhar no WhatsApp
  • Mesmo que não seja possível oferecer funcionalidade real por causa de uma plataforma específica ou de um estado de aposentadoria, para que apps existentes não quebrem é preciso projetar o comportamento de “não fazer nada” dentro do contrato de API documentado
  • Em ambientes sem infraestrutura de impressão, como o Xbox, uma resposta bem-sucedida com 0 impressoras é mais segura para apps escritos com base no PC do que uma NotSupportedException
  • Essa abordagem é uma API inerte: um design que mantém a superfície e a especificação da API, mas não realiza nenhuma operação realmente útil
  • Em uma API de widgets sendo aposentada, se CreateWidget tiver sucesso e retornar nullptr, o chamador passa por um estado contraditório; portanto, o caminho de falha documentado com ERROR_CANCELLED é mais consistente
  • Uma API inerte correta faz apenas a funcionalidade desaparecer e usa caminhos de falha para os quais o código existente já estava preparado, reduzindo exceções, handles inválidos e crashes

Mesmo sem funcionalidade, o contrato da API permanece

  • Há casos em que uma API precisa ser feita para não fazer nada, mas essa inatividade também deve estar de acordo com o código existente e com o comportamento documentado
  • Uma inatividade mal projetada pode gerar exceções, valores de retorno contraditórios e estados inválidos, levando a crashes de apps ou a erros desnecessários para o usuário

Como neutralizar APIs de impressão no Xbox

  • O Windows tem uma ampla infraestrutura de impressão, mas o Xbox não tem essa infraestrutura
  • Se, quando um app tentar imprimir no Xbox, a função de impressão lançar uma NotSupportedException, o problema pode ficar maior
    • É bem provável que os apps instalados no Xbox tenham sido testados principalmente no PC
    • No PC, a impressão está sempre disponível, então uma exceção não tratada pode levar ao crash do app
    • Mesmo que a exceção seja capturada, o usuário pode ver uma mensagem de erro como “forneça o incident code ao suporte”
  • Um design melhor é a função de impressão ter sucesso, mas informar que não há impressoras instaladas
    • O app exibe a UI de seleção de impressora e mostra uma lista vazia
    • O usuário vê que não há impressoras e pode cancelar a solicitação de impressão
  • Para apps que tentam ajudar a instalar uma impressora, a função de instalação pode retornar imediatamente com um código de resultado que significa que o usuário cancelou a operação

Condições de uma API inerte

  • Esse comportamento de “não fazer nada” é chamado de inerte
  • Uma API inerte mantém a superfície da API existente e se comporta conforme a especificação, mas, na prática, não faz nenhuma operação útil
  • O ponto central é não fazer nada de uma forma consistente com a documentação e com a menor probabilidade de causar problemas ao código existente
  • A API de impressão também poderia adicionar uma função para verificar se a impressão em si é possível
    • O app pode usar essa função para ocultar o botão Print em sistemas que não dão suporte algum à impressão
    • Apps que simplesmente assumem que a impressão é possível também passam a se comportar como em “um sistema sem impressoras e em que tentativas de instalação não têm efeito”

Design a evitar em uma API de widgets sendo aposentada

  • A API a ser aposentada tem uma função que cria um handle de widget, uma função que recebe um handle de widget e uma função que fecha um handle de widget
  • A proposta inicial era fazer CreateWidget retornar S_OK e definir *widget = nullptr
    • A chamada teria sucesso, mas não receberia um handle válido
    • Entre o código de teste real, havia código que julgava o sucesso não pelo valor de retorno, mas por verificar se o handle era null
  • Um design em que EnableWidget retorna “handle inválido” também confunde o chamador
    • O app passa para EnableWidget o handle recebido após o sucesso de CreateWidget
    • A API responde que esse handle não é válido
    • O chamador passa por um estado contraditório: “pedi um widget, recebi um, mas quando o mostro de novo dizem que não é um widget”

Design corrigido com um caminho de falha documentado

  • A documentação existente tem o valor de retorno ERROR_CANCELLED, que significa que o usuário cancelou a criação do widget
  • Como o app já precisa lidar com a possibilidade de o widget não ser criado por condições externas, a criação do widget pode ser feita para sempre falhar como cancelamento pelo usuário
  • O fluxo do design revisado é simples
    • CreateWidget deixa *widget = nullptr e retorna HRESULT_FROM_WIN32(ERROR_CANCELLED)
    • Como a criação do widget sempre falha, não existe handle de widget válido
    • GetWidgetAliases, EnableWidget e Close retornam E_HANDLE, indicando que o handle não é válido
  • Nesse método, como não há widget, também não é preciso criar um alias fictício
    • Como não existe widget válido, também não há situação em que o app possa solicitar aliases normalmente

Critério ao levar APIs de desktop para outro ambiente

  • No desktop, a API de impressão sempre existiu, e a função que “obtém a lista de impressoras” é documentada para não lançar exceções
  • Ao levar essa API para o Xbox, para que apps de desktop existentes continuem executando, o comportamento inerte correto é retornar honestamente que “não há impressoras no Xbox”
  • Lançar uma exceção para a pergunta “quantas impressoras há?” não está de acordo com o contrato de API documentado
  • O mesmo princípio se aplica ao aposentar APIs existentes
    • Mesmo que não façam trabalho útil, o comportamento da API deve ser consistente com o contrato
    • É importante usar valores de retorno e estados que o código existente já consiga tratar

2 comentários

 
GN⁺ 2024-02-17
Comentários no Lobste.rs
  • O que está sendo dito aqui é que as funções de impressão se comportam de forma consistente como se a impressão fosse totalmente suportada, mas, estranhamente, nunca haverá de fato uma impressora para imprimir, o que explica muita coisa
    Brincadeiras à parte, não concordo com esse tipo de programação excessivamente defensiva e com essa experiência do usuário. Desse jeito, o software simplesmente deixa de fazer o que deveria sem que ninguém saiba por quê, e não há como descobrir o motivo. O app deveria capturar o erro e, se possível, gerar uma mensagem amigável para o usuário; caso contrário, ao menos mostrar a mensagem de erro original. Se for uma tarefa em segundo plano, deveria haver um log de erros
    Reconheço que este texto foi escrito da perspectiva de um desenvolvedor de API, não de um desenvolvedor de aplicativo. Então, os erros da API devem ser documentados, e devem ser fornecidas mensagens de erro sobre as quais quem chama a API possa agir
    Também não gosto de esconder botões na UI só porque o usuário não tem permissão. Se houver espaço, acho melhor mostrar o botão desativado e, quando o usuário passar o mouse por cima, exibir uma mensagem explicando como ativá-lo
    • Também é preciso levar em conta que este texto não está na perspectiva de um simples desenvolvedor de API, mas de um desenvolvedor da API do Windows. Há muito tempo a posição da Microsoft é que a API do Windows não deve quebrar compatibilidade mesmo quando a versão muda
    • Esta é uma discussão clássica sobre a Lei de Postel / princípio da robustez. Hoje todos já sabem que o princípio da robustez envelheceu mal e gerou monstros como HTML e protocolos e formatos de arquivo enormes nos quais é difícil escrever parsers corretos
      No geral, é melhor exigir correção. Mas, se você tem 1 bilhão de usuários existentes, é muito sensato evitar quebrar as coisas sempre que possível, e isso também gera valor real no nível do sistema porque, do ponto de vista do usuário, simplesmente funciona. No fim, a atitude deveria ser: falhe rápido, mas não faça com que muita coisa falhe
    • Neste caso, uma das APIs existentes provavelmente não tinha um erro documentado, então pode muito bem ter sido criada sem nenhum tratamento de erro desde o início. Se você não quer quebrar todo o software existente, precisa contar uma mentira piedosa
      Isso está mais próximo de estabilidade de ABI do que de API. No Windows, até software compilado há 15 anos deve continuar funcionando em sistemas operacionais novos sempre que possível. Como não dá para mudar a assinatura da função, se você quiser que APIs que já não fazem mais sentido continuem funcionando, precisa contar mentiras piedosas
      Por exemplo, a API ainda finge que o Active Desktop existe, porque a alternativa seria quebrar em massa softwares antigos já existentes
    • Concordo totalmente. Há poucas coisas tão frustrantes quanto ficar procurando um botão que não aparece na minha tela, mas aparece para a pessoa ao lado que está explicando ou no tutorial
      Fica impossível saber se a função foi removida ou se acabou escondida em outra tela nesse meio-tempo
    • Está certo que o app deveria capturar o erro e mostrá-lo ao usuário.
      Mas, se o app não fizer isso, as pessoas que usam esse app vão culpar o Windows. Vão culpar o Windows, não o app, e isso vale até quando o app trava
      É por isso que a Microsoft cria soluções alternativas. É muito mais fácil deixar o trabalho de impressão simplesmente desaparecer, e aí o usuário pensa por um instante e aceita: “ah, é verdade, não tem impressora”
  • Se a função de instalação da impressora puder retornar imediatamente com um código de resultado indicando o usuário cancelou a operação, do ponto de vista de suporte a aplicações isso quase certamente leva a um comportamento indesejado muito mais difícil de lidar do que se a API de impressão simplesmente lançasse uma exceção desde o início
  • Tenho feito bastante programação assistida por AI ultimamente e vejo agentes inserindo com frequência verificações if para checar null. Sempre que vejo isso, lembro deste texto
    Então passei a instruir o agente a não repetir verificações de null, a usar funções inofensivas e a verificar uma única vez, no momento da declaração, que o valor nunca é null
 
GN⁺ 2024-02-17
Opiniões no Hacker News
  • Aprendi isso como uma forma de engolir erros, e vejo como uma prática ruim
    Além de não resolver o problema real de não ser possível imprimir no Xbox, isso esconde o quanto o software está quebrado, tornando a descoberta de bugs e os testes muito mais difíceis
    O panic do Go é bom porque não foi feito para ser abusado ou recuperado em tempo de execução, mas para soar um alarme bem alto, durante os testes, dizendo que o programador fez algo errado
    Quando os atores dentro de um sistema escondem ativamente seus próprios defeitos ou erros, encontrar e corrigir a causa fica exponencialmente mais difícil

    • Projeto de erros depende do contexto
      Nos sistemas B2B, de microsserviços e centrados em APIs com os quais lido, o correto é falhar rapidamente em requisições fora da especificação
      Mas o Windows fez enormes concessões em nome da compatibilidade, chegando a salvar softwares famosos quebrados como SimCity até com patches de memória: https://arstechnica.com/gadgets/2022/10/windows-95-went-the-...
      Em um ambiente desktop em que o usuário não consegue corrigir o código do sistema, muitas vezes a experiência do usuário final é mais importante que a correção
      Não importa por que ocorreu uma BSOD; o que a Microsoft aprendeu é que software ou hardware ruim prejudica a reputação da Microsoft
      Pessoalmente, prefiro projetos que falham rápido ou interrompem a execução diante de entradas ou condições inválidas, mas entendo o valor dessa abordagem nesse ambiente
      O importante é o contexto, não aplicar dogmaticamente um lado ou outro
      Por exemplo, não se deve levar a abordagem de Chen para o projeto de um reator nuclear ou de trading de alta frequência, e ela pode ser uma ótima abordagem para um motor de jogo
    • Ao projetar um runtime alternativo para uma nova plataforma, o ponto central é decidir por conta própria o que será considerado erro
      Decidir se um componente do runtime ficará “inofensivamente inativo” é uma decisão de projeto que vem antes de a implementação dar panic ou não
      Neste caso, cabe à Microsoft definir o significado de “imprimir no Xbox”
      Isso pode ser definido como um erro, ou, em teoria, como um recurso que pode ser suportado no futuro, mas que hoje não tem uma forma real de ser executado
      Se alguém conectar uma impressora USB e o “jogo” souber como imprimir, como pela porta de expansão de jogos de Game Boy que davam suporte ao GB Printer, em teoria seria possível imprimir até no Xbox
      Um jogo de Xbox tentar imprimir não é necessariamente um erro intrínseco; defini-lo como erro também é apenas uma escolha com trade-offs, como portabilidade de aplicações
      Você poderia exibir uma tela de seleção sem impressoras disponíveis, como no Xbox, fazer a API mentir dizendo que imprimiu, ou dar suporte à impressão real
      Só depois de definir o significado de “imprimir no Xbox” como erro é que o fato de esse erro não ser lançado e ser engolido se torna um problema de implementação ou depuração
    • Isso não é engolir erros; em termos do Linux, está mais próximo de não quebrar o espaço de usuário
      Há dois tratamentos possíveis: como a máquina nunca terá uma impressora, retornar um erro; ou, como a máquina nunca terá uma impressora, retornar uma lista vazia de impressoras
      Ambos são válidos, mas só um deles não quebra o espaço de usuário
    • Lendo o texto, ele fala mais de experiência do usuário do que de experiência do desenvolvedor
      “Soar um alarme bem alto” é bom para testadores ou desenvolvedores, mas não é muito bom para o usuário final
      Do ponto de vista do usuário final, engolir o erro é melhor do que o app morrer
    • É irritante que, há décadas, quando uma conexão com a internet falha, apareça apenas uma mensagem dizendo que falhou
      Não dá para saber o que falhou: o software do meu computador, o hardware, o cabo Ethernet ou o Wi-Fi, o switch, o roteador, o modem a cabo, o cabo de internet que chega até a minha casa, o ISP, ou o sistema remoto que estou tentando acessar; nada disso melhorou
  • É esperado que haja uma reação negativa, mas esse tipo de coisa bagunçada é justamente o motivo de um documento do Word ’97 ou de um jogo compilado há 30 anos para MS-DOS abrir como esperado quando você clica nele
    Compatibilidade retroativa é sempre messy, e ou ela é feita de forma imperfeita, ou simplesmente não é feita

    • Ouço esse refrão sobre a Microsoft com frequência, mas em jogos não tive muita sorte
      No fim acabo usando o GOG, e o GOG também acaba virtualizando o ambiente
      Por exemplo, não acho que alguém vá conseguir instalar o Sim City original no Windows 11 com sucesso
    • Um arquivo criado no Word ’97 já não abria exatamente como esperado em dois computadores diferentes nem 25 anos atrás
      A formatação dependia do driver da impressora instalado
      Por outro lado, um arquivo LaTeX de 35 anos atrás ainda funciona bem hoje
    • Mesmo para um arquivo de 1997, é preciso ter sorte, e há uma boa chance de ele simplesmente aparecer errado em qualquer app de escritório
      O Word não conseguia garantir que a formatação não mudaria nem ao reabrir no mesmo computador e na mesma versão, e foi por isso que aprendi TeX
      Nos jogos também, a Microsoft quebrou inúmeros jogos ao encerrar o GFWL
      O Dark Souls original não pôde ser jogado até ser relançado no Steam, então era preciso usar uma cópia pirata
    • Softwares de MS-DOS não funcionam no Windows 64 bits moderno
      Aparece uma caixa de diálogo dizendo “não suportado”, e você pode baixar o dosbox, mas isso não é “exatamente como esperado”
    • Eu gostaria que isso fosse verdade, mas tive um trabalhão para rodar os jogos que guardei da infância
      Quase sempre desisti ou comprei de novo no GOG; na prática, paguei pelo trabalho de alguém que já tinha resolvido os problemas de compatibilidade
  • Nada é tão frustrante quanto uma UI de dispositivo que sugere que algo poderia existir de fato, mas agora diz que não há nada
    No fim, só depois de gastar tempo é que você descobre que aqueles dispositivos não são compatíveis e que aquela tela era apenas um mockup feito por alguém

    • Ainda assim, provavelmente seria menos frustrante do que o app simplesmente morrer
      Se o app não previu a situação de impressão sem suporte e a impressão lança uma exceção, o app provavelmente morre ali mesmo
      Um app que previu a falta de suporte à impressão deveria primeiro verificar, por uma API explícita, se a impressão é suportada e, se não for, não deveria mostrar a UI de impressão
      O texto não fala desse tipo de app, mas de como lidar com apps que não fazem essa verificação antes
      O bug está no app, mas uma boa plataforma deve tentar fazer até apps ruins funcionarem o máximo possível, em vez de simplesmente deixá-los morrer
    • Aqui, a responsabilidade maior pode não ser da Microsoft, mas do autor do software que não tratou a situação da impressora
      O papel da Microsoft é impedir que o app morra só porque o fornecedor do software não levou impressoras em consideração
  • Quando um app tenta imprimir, pedir ao usuário que escolha uma impressora e mostrar uma lista vazia — parece que a Microsoft não aprendeu em 30 anos: https://www.reddit.com/r/hacking/comments/djvzd/windows_nt_l...

  • A direção concreta proposta no texto — isto é, que o componente deve sofrer antes do usuário sofrer — está certa, mas o enquadramento incomoda bastante
    Coisas como “há momentos em que é preciso fazer a API não fazer nada” ou “é errado uma função de impressão lançar NotSupportedException” definitivamente não são conselhos gerais
    Isso é um hack para dar suporte a clientes péssimos e, embora às vezes seja necessário, não é um conselho normal nem generalizável
    Esse tipo de formulação mostra sinais de ter internalizado o sofrimento de ser desenvolvedor da Microsoft

    • Não é um cliente péssimo, é um cliente escrito antes de você decidir essa mudança
      Se o cliente não mudou e a API o quebra, então quem é péssima não é o cliente, mas a API
    • Se uma plataforma de software foi adotada por mais do que um punhado de desenvolvedores, inevitavelmente surgirão apps ruins, e aí será preciso fazer esse tipo de coisa
      Em qualquer plataforma amplamente adotada e que leve compatibilidade retroativa a sério, isso é algo completamente normal e generalizável
      O Windows é o exemplo clássico, mas o mesmo acontece no ABI do kernel Linux, na glibc, na plataforma web, no Java etc.
    • Há momentos em que é preciso implementar uma API que clientes existentes já estão usando
      Não dá para voltar no tempo e recompilar ou reescrever esses clientes
      Se o cliente já está verificando NotSupportedException, você deve retorná-la; se não estiver, precisa de outra abordagem
    • Se alguém tivesse essa atitude dentro da equipe do Windows, se eu fosse gerente talvez o demitisse
      Isto é Windows
      Você não pode quebrar aplicativos dos quais os usuários dependem
      Se há um app cliente “péssimo” ou que se comporta incorretamente, você precisa usar todos os contornos, shims e hacks de compatibilidade necessários para fazê-lo funcionar como esperado
      Mesmo que isso signifique colocar código especial de gerenciamento de memória para fazer SimCity funcionar
    • Esse “cliente péssimo” pode ter sido escrito por uma empresa que fechou há muito tempo
      Seu cliente depende dele e, se ele parar por causa de algo que você mudou, vai culpar você
  • Aqui todo mundo fica preso em “não fazer nada” e “tratamento de erros”, mas parece que o texto não explicou bem que não está propondo engolir todos os problemas e cantar kumbaya
    O contexto real é emulação e compatibilidade para software que não vai mudar, algo muito diferente da maioria das situações
    A própria Microsoft costuma borrar a fronteira entre as duas coisas, o que aumenta a confusão, mas o assunto aqui não é tratamento de erros em geral
    Emulação é, por natureza, mentir
    Não há como conectar uma impressora de verdade a um Xbox, você está rodando um jogo de Windows no Linux, e o iPhone falso pode na verdade ser um servidor ARM da AWS
    Se você vai mentir, precisa mentir de forma convincente, para que os aplicativos existentes continuem funcionando
    Quando a Microsoft diz que é possível portar apps Windows existentes para o Xbox, havia nos apps Windows a promessa de que era possível imprimir; portanto, o Xbox precisa mentir para fazer isso parecer possível
    Por outro lado, se um programa está fazendo use-after-free e você contorna silenciosamente fingindo que não há bug, isso não é o tratamento correto
    Afinal, ninguém prometeu que até use-after-free existente teria compatibilidade bug a bug na nova plataforma

    • Em nenhum lugar do texto há o contexto de fazer emulação para software que não vai mudar
      Essa interpretação foi apenas assumida porque sustenta a posição que você quer defender
      Na prática, o conteúdo do texto também se aplica exatamente igual se eu escrever amanhã um programa para Xbox e tentar imprimir
      Não há nada distinguindo que esse comportamento se aplique apenas a software antigo e sem manutenção
  • Gosto e não gosto dessa abordagem
    Instintivamente, não gosto de lidar com problemas por meio de cumprimento malicioso
    Por outro lado, se o objetivo é fazer com que mais usuários rodem mais software na plataforma, mesmo que a impressão esteja quebrada, concordo totalmente que é uma boa decisão

  • Este fluxo de comentários foi estranho
    Claramente há gente que odeia qualquer coisa que a Microsoft faça
    O ponto central do blog é tornar sem atrito o processo de levar apps (UWP) para o XBOX
    Fazer o app “simplesmente funcionar”, sem recompilação, sem ifdef sujo, sem trabalho adicional, e então o desenvolvedor pode ajustar o app à plataforma XBOX
    O importante é que o desenvolvedor conseguiu levar o app para o XBOX sem esforço nenhum, e isso é bom tanto para a plataforma quanto para os desenvolvedores
    Para o usuário, a lista de impressoras vazia é um sinal claro de que não é possível imprimir
    Acima de tudo, o app não morre nem mostra um erro péssimo como “Ocorreu um problema. Tente novamente mais tarde”
    Essa mensagem não é uma experiência melhor para o usuário
    O desenvolvedor pode recompilar o app e inserir uma mensagem mais explicativa, mas aí voltamos à estaca zero

    • 95% desta thread parece não ter entendido o texto
      Por exemplo, a acusação de que ele engole erros não tem nada a ver com o ponto central
  • O ponto central que quem critica este texto deixa passar é que aqui não há uma situação excepcional que exija lançar uma exceção
    Não há uma impressora conectada a este dispositivo e, neste caso, isso é verdade por definição; portanto, o app deve tratar essa situação de forma limpa, não morrer
    Não se lançaria uma exceção só porque um notebook não consegue acessar uma impressora

    • Se você quiser um exemplo moderno de um produto não Microsoft amplamente usado que lida mal com a disponibilidade de impressoras, há o gnome-control-center
      Depois de adicionar uma impressora nas configurações, se você mudar para a aba “Color Profiles” durante a instalação, no timing certo o gnome-control-center morre
      Olhando mais de perto, ele tentou enumerar as impressoras disponíveis; sabe que deveria haver uma impressora, mas as informações ainda não existem, então simplesmente morre
      Felizmente, basta esperar alguns segundos até o GNOME terminar de instalar a impressora e abrir as configurações de novo
      Mesmo assim, se é um app essencial que permite a usuários finais de diferentes perfis usar o desktop corretamente, em um mundo ideal ele nunca deveria morrer
      Pensar em condições de contorno é difícil, e imaginar que o próprio tratamento de condições de contorno também tem outras condições de contorno é ainda mais difícil
      Certo ou errado, pelo menos o autor está pensando mais a fundo do que “é um app ruim, então precisa ser reescrito para dar suporte ao Wayland”
  • Houve uma época em que era considerado uma ótima estratégia o navegador fazer o melhor possível para exibir uma página mesmo quando havia erros no código HTML
    Era algo como seguir em frente tentando adivinhar ao máximo a intenção do autor, partindo da ideia de que erros são ruins e usuários não querem erros
    Eu achava que tínhamos aprendido com essa experiência, mas aparentemente não

    • Os navegadores ainda seguem essa estratégia
      A tentativa do XHTML de substituir isso por validação estrita fracassou; em vez disso, o HTML5, que teve sucesso, voltou atrás e definiu explicitamente como interpretar todas as sequências inválidas possíveis, para que os navegadores ao menos se comportassem de forma consistente
    • Foi graças à tolerância dos navegadores com as entradas que a web moderna se tornou possível
      Se os primeiros navegadores exibissem um erro assim que encontrassem uma tag desconhecida, novos recursos dos navegadores teriam sido sufocados no nascimento
    • Ao inserir Hello e World!, dá para ver se, graças à sabedoria que adquirimos, agora aparece um erro
      Mas, se ainda renderiza bem, isso pode significar que ainda há muito a aprender, ou que também faz sentido tratar certos tipos de erro com elegância e morrer em situações excepcionais
    • O avanço do software nos últimos 20 anos, com exceção dos LLMs, não foi lá grande coisa, e os LLMs também foram estragados pela censura
      Acho que, se eu viajasse no tempo para 1999, não sentiria tanta falta assim
    • Fico curioso sobre o que achávamos que tínhamos aprendido