Diagramas de bloqueio na prática sem formalina

O diagrama de blocos é uma ferramenta visual que ajuda a transformar um algoritmo complexo em uma sequência de ações compreensíveis e estruturadas. Da programação ao gerenciamento de processos de negócios, eles servem como uma linguagem universal para visualização, análise e otimização dos sistemas mais complexos.

Imagine um mapa onde, em vez de estradas, é lógica e, em vez de cidades – ações. Esta é um diagrama de blocos-uma ferramenta indispensável para navegação nos processos mais confusos.

Exemplo 1: Esquema de lançamento de jogo simplificado
Para entender o princípio do trabalho, vamos apresentar um simples esquema de lançamento de jogo.

Esse esquema mostra o script perfeito quando tudo acontece sem falhas. Mas na vida real, tudo é muito mais complicado.

Exemplo 2: Esquema expandido para iniciar o jogo com carregamento de dados
Os jogos modernos geralmente exigem conexão à Internet para baixar dados, salvamento ou configurações do usuário. Vamos adicionar essas etapas ao nosso esquema.

Esse esquema já é mais realista, mas o que acontecerá se algo der errado?

Como foi: um jogo que “quebrou” com a perda da Internet

No início do projeto, os desenvolvedores não puderam levar em consideração todos os cenários possíveis. Por exemplo, eles se concentraram na lógica principal do jogo e não pensaram no que aconteceria se o jogador tivesse uma conexão com a Internet.

Em tal situação, o diagrama de blocos de seu código ficaria assim:

Nesse caso, em vez de emitir um erro ou fechar corretamente, o jogo congelou na fase de espera por dados que ela não recebeu devido à falta de uma conexão. Isso levou à “tela preta” e congelando o aplicativo.

Como se tornou: correção nas reclamações de usuários

Após inúmeras reclamações dos usuários sobre pairando, a equipe do desenvolvedor percebeu que precisávamos corrigir o erro. Eles fizeram alterações no código adicionando uma unidade de processamento de erros que permite que o aplicativo responda corretamente à falta de conexão.

É assim que o diagrama de blocos corrigido se parece, onde ambos os cenários são levados em consideração:

Graças a essa abordagem, o jogo agora informa corretamente o usuário sobre o problema e, em alguns casos, ele pode até ir para o modo offline, permitindo que você continue o jogo. Este é um bom exemplo de por que os diagramas de blocos são tão importantes : eles fazem o desenvolvedor pensar não apenas sobre a maneira ideal de execução, mas também sobre todas as falhas possíveis, tornando o produto final muito mais estável e confiável.

comportamento incerto

Pendurado e erros são apenas um exemplos de comportamento imprevisível do programa. Na programação, existe um conceito de comportamento incerto (comportamento indefinido) – Esta é uma situação em que o padrão do idioma não descreve como o programa deve se comportar em um determinado caso.

Isso pode levar a qualquer coisa: do “lixo” aleatório na retirada ao fracasso do programa ou mesmo à séria vulnerabilidade de segurança. O comportamento indefinido geralmente ocorre ao trabalhar com memória, por exemplo, com linhas na linguagem de C.

Um exemplo do idioma c:

Imagine que o desenvolvedor copiou a linha para o buffer, mas esqueceu de adicionar ao final o símbolo zero (`\ 0`) , que marca o final da linha.

É assim que o código se parece:

#include 

int main() {
char buffer[5];
char* my_string = "hello";

memcpy(buffer, my_string, 5);

printf("%s\n", buffer);
return 0;
}

Resultado esperado: “Olá”
O resultado real é imprevisível.

Por que isso está acontecendo? A função `printf` com o especificador`%s` espera que a linha termine com um símbolo zero. Se ele não estiver, ela continuará lendo a memória fora do buffer destacado.

Aqui está o diagrama de blocos deste processo com dois resultados possíveis:

Este é um exemplo claro de por que os diagramas de blocos são tão importantes: eles fazem o desenvolvedor pensar não apenas sobre a maneira ideal de execução, mas também sobre todas as falhas possíveis, incluindo problemas de baixo nível, tornando o produto final muito mais estável e confiável.

Pixel perfeito: mito ou realidade na era da declaratividade?

No mundo do desenvolvimento de interfaces, existe um conceito comum – “Pixel perfeito no alojamento” . Isso implica a reprodução mais precisa da máquina de design ao menor pixel. Durante muito tempo, era um padrão -ouro, especialmente na era de um design clássico da web. No entanto, com a chegada da milha declarativa e o rápido crescimento da variedade de dispositivos, o princípio de “Pixel Perfect” está se tornando mais efêmero. Vamos tentar descobrir o porquê.

Imperial Wysiwyg vs. Código declarativo: Qual é a diferença?

Tradicionalmente, muitas interfaces, especialmente o desktop, eram criadas usando abordagens imperativas ou wysiwyg (o que você vê é o que recebe) dos editores. Nessas ferramentas, o designer ou desenvolvedor manipula diretamente com elementos, colocando -os em tela com precisão no pixel. É semelhante ao trabalho com um editor gráfico – você vê como o seu elemento se parece e definitivamente pode posicioná -lo. Nesse caso, a conquista de “Pixel Perfect” era um objetivo muito real.

No entanto, o desenvolvimento moderno é cada vez mais baseado em milhas declarativas . Isso significa que você não diz ao computador para “colocar este botão aqui”, mas descreva o que deseja obter. Por exemplo, em vez de indicar as coordenadas específicas do elemento, você descreve suas propriedades: “Este botão deve ser vermelho, ter recua de 16px de todos os lados e estar no centro do contêiner”. Freimvorki como React, Vue, Swiftui ou Jetpack Compose apenas use esse princípio.

Por que “Pixel Perfect” não funciona com uma milha declarativa para muitos dispositivos

Imagine que você cria um aplicativo que deve parecer igualmente bom no iPhone 15 Pro Max, Samsung Galaxy Fold, iPad Pro e uma resolução 4K. Cada um desses dispositivos possui resolução de tela diferente, densidade de pixels, partes e tamanhos físicos.

Quando você usa a abordagem declarativa, o próprio sistema decide como exibir a interface descrita em um dispositivo específico, levando em consideração todos os seus parâmetros. Você define as regras e dependências, não coordenadas duras.

* Adaptabilidade e capacidade de resposta: O principal objetivo das milhas declarativas é criar interfaces adaptativas e responsivas . Isso significa que sua interface deve se adaptar automaticamente ao tamanho e orientação da tela sem quebrar e manter a legibilidade. Se procurássemos “pixel perfeito” em cada dispositivo, teríamos que criar inúmeras opções para a mesma interface, o que nivelará completamente as vantagens da abordagem declarativa.
* densidade de pixel (DPI/PPI): Os dispositivos têm densidade de pixels diferentes. O mesmo elemento com o tamanho de 100 pixels “virtuais” em um dispositivo com alta densidade parecerá muito menor do que em um dispositivo de baixa densidade, se você não levar em consideração a escala. As estruturas declarativas são abstraídas por pixels físicos, trabalhando com unidades lógicas.
* Conteúdo dinâmico: em aplicativos modernos geralmente é dinâmico – seu volume e estrutura podem variar. Se embutirmos com força para os pixels, qualquer alteração no texto ou imagem levaria ao “colapso” do layout.
* Várias plataformas: Além da variedade de dispositivos, existem diferentes sistemas operacionais (iOS, Android, Web, Desktop). Cada plataforma possui seu próprio design, controles padrão e fontes. Uma tentativa de fazer uma interface perfeita de pixel absolutamente idêntica em todas as plataformas levaria a um tipo não natural e uma experiência de usuário ruim.

As abordagens antigas não foram embora, mas evoluíram

É importante entender que a abordagem das interfaces não é uma escolha binária entre “imperativo” e “declarativo”. Historicamente, para cada plataforma, havia suas próprias ferramentas e abordagens para a criação de interfaces.

* Arquivos de interface nativos: Para iOS, eram xib/storyboards, para arquivos de marcação Android-xml. Esses arquivos são um layout wysiwyg perfeito para pixels, que é exibido no rádio como no editor. Essa abordagem não desapareceu em nenhum lugar, continua a se desenvolver, integrando -se com quadros declarativos modernos. Por exemplo, SwiftUi na Apple e Jetpack compor no Android partiu no caminho de um código puramente declarativo, mas, ao mesmo tempo, manteve a oportunidade de usar um layout clássico.
* Soluções híbridas: Em projetos reais, é usada uma combinação de abordagens. Por exemplo, a estrutura básica do aplicativo pode ser implementada declarativamente e, para específicos, exigindo posicionamento preciso de elementos, métodos imperativos de nível inferior, podem ser usados ou componentes nativos desenvolvidos levando em consideração as especificidades da plataforma.

Do monólito à adaptabilidade: como a evolução dos dispositivos formou uma milha declarativa

O mundo das interfaces digitais passou por tremendas mudanças nas últimas décadas. De computadores estacionários com licenças fixas, chegamos à era do crescimento exponencial da variedade de dispositivos de usuário . Hoje, nossos aplicativos devem funcionar igualmente bem em:

* smartphones de todos os fatores de forma e tamanhos de tela.
* comprimidos com seus modos de orientação exclusivos e uma tela separada.
* Laptops e desktops com várias licenças de monitores.
* TVs e centros de mídia , controlados remotamente. Vale ressaltar que, mesmo para as TVs, cujas observações podem ser simples como Apple TV Remote com um mínimo de botões, ou vice -versa, sobrecarregados com muitas funções, os requisitos modernos para interfaces são tais que o código não exija adaptação específica para esses recursos de entrada. A interface deve funcionar “como se por si só”, sem uma descrição adicional do que “como” interagir com um controle remoto específico.
* relógios inteligentes e dispositivos vestíveis com telas minimalistas.
* Capacetes de realidade virtual (VR) , exigindo uma abordagem completamente nova para uma interface espacial.
* Dispositivos de realidade aumentada (AR) , aplicando informações sobre o mundo real.
* Informações de automóveis e sistemas de entretenimento .
* E até eletrodomésticos : de geladeiras com telas sensoriais e máquinas de lavar com displays interativos para fornos e sistemas inteligentes da casa inteligente.

Cada um desses dispositivos possui seus próprios recursos exclusivos: dimensões físicas, proporção de partes, densidade de pixels, métodos de entrada (tela de toque, mouse, controladores, gestos, comandos vocais) e, principalmente, as sutilezas do ambiente do usuário . Por exemplo, um shlesh de VR requer uma imersão profunda e um trabalho intuitivo e rápido do smartphone em movimento, enquanto a interface da geladeira deve ser tão simples e grande para navegação rápida.

Abordagem clássica: o ônus de apoiar interfaces individuais

Na era do domínio dos desktops e dos primeiros dispositivos móveis, o negócio usual era a criação e o suporte de arquivos de interface individuais ou mesmo um código de interface completamente separado para cada plataforma .

* O desenvolvimento em iOS geralmente exigia o uso de storyboards ou arquivos XIB no Xcode, escrevendo código no Objective-C ou Swift.
* Para Android Os arquivos de marcação XML e o código em Java ou Kotlin foram criados.
* Interfaces da Web ativadas em HTML/CSS/JavaScript.
* Para aplicativos C ++ Em várias plataformas de desktop, foram usadas suas estruturas e ferramentas específicas:
* Em Windows Estes foram MFC (Microsoft Foundation Classes), API Win32 com elementos de desenho manual ou usando arquivos de recursos para janelas de diálogo e elementos de controle.
* Cacau (Objective-C/Swift) ou A API de carbono antigo para controle direto da interface gráfica foram usados no macOS .
* Nos sistemas Linux/UNIX , bibliotecas como GTK+ ou QT foram frequentemente usadas, o que forneceu seu conjunto de widgets e mecanismos para criar interfaces, geralmente por meio de arquivos de marcação do tipo XML (por exemplo, arquivos .ui no designer QT) ou criação de software direto de elementos.

Essa abordagem garantiu o controle máximo sobre cada plataforma, permitindo que você levasse em consideração todos os seus recursos específicos e elementos nativos. No entanto, ele teve uma enorme desvantagem: duplicação de esforços e enormes custos de apoio . A menor mudança no design ou funcionalidade exigia a introdução de um direito a vários, de fato, bases de código independentes. Isso se transformou em um pesadelo real para as equipes de desenvolvedores, diminuindo a desaceleração da produção de novas funções e aumentando a probabilidade de erros.

Miles declarativos: um único idioma para a diversidade

Foi uma resposta a essa rápida complicação que as milhas declarativas apareceram como o paradigma dominante. Framws como React, Vue, Swiftui, Jetpack compõem e outros não são apenas uma nova maneira de escrever código, mas uma mudança fundamental no pensamento.

A idéia principal da abordagem declarativa : em vez de dizer o sistema “como” desenhar todos os elementos (imperativos), descrevemos “o que” queremos ver (declarativo). Definimos as propriedades e a condição da interface, e a estrutura decide como exibi -la melhor em um dispositivo específico.

Isso se tornou possível graças às seguintes vantagens importantes:

1. Abstração dos detalhes da plataforma: O Fraimvorki declarativo é especialmente projetado para esquecer os detalhes de baixo nível de cada plataforma. O desenvolvedor descreve os componentes e seus relacionamentos em um nível mais alto de abstração, usando um único código transferido.
2. Adaptação e capacidade de resposta automáticas: Freimvorki assume a responsabilidade pela escala automática, alterando o layout e a adaptação dos elementos para diferentes tamanhos de telas, densidade de pixels e métodos de entrada. Isso é conseguido através do uso de sistemas de layout flexíveis, como Flexbox ou grade, e conceitos semelhantes a “pixels lógicos” ou “dp”.
3. Consistência da experiência do usuário: Apesar das diferenças externas, a abordagem declarativa permite manter uma única lógica de comportamento e interação em toda a família de dispositivos. Isso simplifica o processo de teste e fornece uma experiência mais previsível do usuário.
4. A aceleração do desenvolvimento e redução de custos: Com o mesmo código capaz de trabalhar em muitas plataformas, significativamente é reduzido pelo tempo e custo de desenvolvimento e suporte . As equipes podem se concentrar na funcionalidade e no design, e não na reescrita repetida na mesma interface.
5. prontidão para o futuro: A capacidade de abstrair das especificidades dos dispositivos atuais torna o código declarativo mais mais resistente ao surgimento de novos tipos de dispositivos e fatores de forma . O Freimvorki pode ser atualizado para oferecer suporte a novas tecnologias, e seu código já escrito receberá esse suporte é relativamente perfeito.

Conclusão

A milha declarativa não é apenas uma tendência de moda, mas a etapa evolutiva necessária causada pelo rápido desenvolvimento de dispositivos de usuário, incluindo a esfera da a Internet das Coisas (IoT) e eletrodomésticos inteligentes. Ele permite que desenvolvedores e designers criem interfaces complexas, adaptativas e uniformes, sem se afogar em inúmeras implementações específicas para cada plataforma. A transição do controle imperativo sobre cada pixel para a descrição declarativa do estado desejado é um reconhecimento de que no mundo das interfaces futuras deve ser flexível, transferido e intuitivo independentemente de qual tela são exibidas.

Programadores, designers e usuários precisam aprender a viver neste novo mundo. Os detalhes extras do pixel perfeito, projetados para um dispositivo ou resolução específica, levam a custos de tempo desnecessários para desenvolvimento e suporte. Além disso, esses layouts severos podem simplesmente não funcionar em dispositivos com interfaces não padrão, como TVs de entrada limitadas, mudanças de VR e AR, bem como outros dispositivos do futuro, que ainda nem conhecemos hoje. Flexibilidade e adaptabilidade – essas são as chaves para a criação de interfaces bem -sucedidas no mundo moderno.

Por que os programadores não fazem nada, mesmo com redes neurais

Hoje, as redes neurais são usadas em todos os lugares. Os programadores os usam para gerar código, explicar outras soluções, automatizar tarefas de rotina e até criar aplicativos inteiros do zero. Parece que isso deve levar a um aumento de eficiência, reduzindo erros e aceleração do desenvolvimento. Mas a realidade é muito mais prosaica: muitos ainda não conseguem. As redes neurais não resolvem problemas importantes – elas apenas iluminam a profundidade da ignorância.

dependência total do LLM em vez de entender

A principal razão é que muitos desenvolvedores confiam completamente no LLM, ignorando a necessidade de uma compreensão profunda das ferramentas com as quais trabalham. Em vez de estudar documentação – uma solicitação de bate -papo. Em vez de analisar os motivos do erro – copiar a decisão. Em vez de soluções arquitetônicas – a geração de componentes de acordo com a descrição. Tudo isso pode funcionar em um nível superficial, mas assim que surgir uma tarefa não padrão, integração com um projeto real ou a necessidade de ajuste fino é necessário, tudo está desmoronando.

Falta de contexto e práticas desatualizadas

As redes neurais geram o código generalizado. Eles não levam em consideração os detalhes da sua plataforma, versão das bibliotecas, restrições ambientais ou soluções arquitetônicas do projeto. O que é gerado geralmente parece plausível, mas não tem nada a ver com o código real e suportado. Mesmo recomendações simples podem não funcionar se pertencem à versão desatualizada da estrutura ou abordagens de uso que há muito são reconhecidas como ineficazes ou inseguras. Os modelos não entendem o contexto – eles dependem de estatísticas. Isso significa que erros e antipattterns, populares em código aberto, serão reproduzidos repetidamente.

redundância, ineficiência e falta de perfil

O código gerado pela IA geralmente é redundante. Inclui dependências desnecessárias, duplica a lógica, adiciona abstrações desnecessariamente. Acontece que uma estrutura pesada e ineficaz que é difícil de apoiar. Isso é especialmente agudo no desenvolvimento móvel, onde o tamanho da gangue, o tempo de resposta e o consumo de energia são críticos.

A rede neural não conduz o perfil, não leva em consideração as restrições da CPU e da GPU, não se importa com os vazamentos da memória. Ele não analisa a eficácia do código na prática. A otimização ainda é feita à mão, exigindo análise e exame. Sem ele, o aplicativo se torna lento, instável e intensivo em recursos, mesmo que pareça “certo” do ponto de vista da estrutura.

vulnerabilidade e uma ameaça à segurança

Não se esqueça da segurança. Já existem casos conhecidos em que projetos parcial ou totalmente criados usando LLM foram invadidos com sucesso. Os motivos são típicos: o uso de funções inseguras, falta de verificação dos dados de entrada, erros na lógica da autorização, vazamento por meio de dependências externas. A rede neural pode gerar um código vulnerável simplesmente porque foi encontrado em repositórios abertos. Sem a participação de especialistas em segurança e uma revisão completa, esses erros se tornam facilmente pontos de entrada para ataques.

A lei é pareto e a essência das falhas

A lei de Pareto funciona claramente com as redes neurais: 80% do resultado é alcançado devido a 20% do esforço. O modelo pode gerar uma grande quantidade de código, criar a base do projeto, espalhar a estrutura, organizar tipos, conectar módulos. No entanto, tudo isso pode estar desatualizado, incompatível com versões atuais de bibliotecas ou estruturas, e requer revisão manual significativa. A automação aqui funciona como um rascunho que precisa ser verificado, processado e adaptado a realidades específicas do projeto.

CUIDADO Otimismo

No entanto, o futuro parece encorajador. Atualização constante dos conjuntos de dados de treinamento, integração com documentação atual, verificações automatizadas de arquitetura, conformidade com padrões de design e segurança – tudo isso pode alterar radicalmente as regras do jogo. Talvez em alguns anos possamos realmente escrever o código mais rápido, mais seguro e eficiente, confiando no LLM como um verdadeiro co -autor técnico. Mas por enquanto – infelizmente – muito precisa ser verificado, reescrito e modificado manualmente.

As redes neurais são uma ferramenta poderosa. Mas, para que ele trabalhe para você, e não contra você, você precisa de uma base, pensamento crítico e vontade de assumir o controle a qualquer momento.

Truques de vibe-core: por que o LLM ainda não funciona com sólido, seco e limpo

Com o desenvolvimento de grandes modelos de idiomas (LLM), como ChatGPT, mais e mais desenvolvedores os usam para gerar código, projetar arquitetura e acelerar a integração. No entanto, com a aplicação prática, torna -se perceptível: os princípios clássicos da arquitetura – sólidos, secos, limpos – se dão mal com as peculiaridades da gordura do LLM.

Isso não significa que os princípios estejam desatualizados – pelo contrário, eles funcionam perfeitamente com o desenvolvimento manual. Mas com o LLM, a abordagem deve ser adaptada.

Por que o LLM não pode lidar com os princípios arquitetônicos

Encapsulamento

O incapsolamento requer a compreensão dos limites entre partes do sistema, conhecimento sobre as intenções do desenvolvedor, bem como seguem restrições estritas de acesso. O LLM geralmente simplifica a estrutura, torna o Fields público sem motivo ou duplica a implementação. Isso torna o código mais vulnerável a erros e viola os limites arquitetônicos.

Resumos e interfaces

Os padrões de design, como uma fábrica ou estratégia abstrata, requerem uma visão holística do sistema e entender sua dinâmica. Os modelos podem criar uma interface sem um objetivo claro sem garantir sua implementação ou violar a conexão entre as camadas. O resultado é uma arquitetura excessiva ou não funcional.

seco (não se repete)

O LLM não procura minimizar o código de repetição – pelo contrário, é mais fácil para eles duplicarem blocos do que fazer lógica geral. Embora eles possam oferecer refatoração mediante solicitação, os modelos padrão tendem a gerar fragmentos “auto -suficientes”, mesmo que isso leve à redundância.

Arquitetura limpa

A limpeza implica uma hierarquia estrita, independência das estruturas, dependência direcionada e conexão mínima entre as camadas. A geração de tal estrutura requer uma compreensão global do sistema – e o trabalho de LLM no nível de probabilidade de palavras, não a integridade arquitetônica. Portanto, o código é misturado, com a violação das direções da dependência e uma divisão simplificada em níveis.

O que funciona melhor ao trabalhar com LLM

Molhado em vez de seco
A abordagem molhada (escreva tudo duas vezes) é mais prática para trabalhar com o LLM. A duplicação do código não requer contexto do modelo de retenção, o que significa que o resultado é previsível e é mais fácil de corretamente correto. Também reduz a probabilidade de conexões e bugs não óbvios.

Além disso, a duplicação ajuda a compensar a lembrança curta do modelo: se um certo fragmento de lógica for encontrado em vários lugares, é mais provável que o LLM leve em consideração com mais geração. Isso simplifica o acompanhamento e aumenta a resistência ao “esquecimento”.

estruturas simples em vez de encapsulamento

Evitando encapsulamento complexo e confiar na transmissão direta de dados entre as partes do código, você pode simplificar bastante a geração e a depuração. Isto é especialmente verdade com um rápido desenvolvimento iterativo ou criação de MVP.

Arquitetura simplificada

Uma estrutura simples e plana do projeto com uma quantidade mínima de dependências e abstrações fornece um resultado mais estável durante a geração. O modelo adapta esse código mais fácil e menos frequentemente viola as conexões esperadas entre os componentes.

Integração SDK – manualmente confiável

A maioria dos modelos de idiomas é treinada em versões desatualizadas de documentação. Portanto, ao gerar instruções para a instalação do SDK, os erros geralmente aparecem: comandos desatualizados, parâmetros irrelevantes ou links para recursos inacessíveis. Mostra de prática: é melhor usar documentação oficial e ajuste manual, deixando o LLM uma função auxiliar – por exemplo, gerando um código de modelo ou adaptação de configurações.

Por que os princípios ainda funcionam – mas com desenvolvimento manual

É importante entender que as dificuldades de sólido, seco e limpo dizem respeito à código de código através do LLM. Quando o desenvolvedor escreve o código manualmente, esses princípios continuam demonstrando seu valor: reduzem a conexão, simplificam o suporte, aumentam a legibilidade e a flexibilidade do projeto.

Isso se deve ao fato de que o pensamento humano é propenso à generalização. Estamos procurando padrões, trazemos a lógica repetida para entidades individuais, criamos padrões. Provavelmente, esse comportamento tem raízes evolutivas: reduzir a quantidade de informações salva recursos cognitivos.

O LLM age de maneira diferente: eles não experimentam cargas do volume de dados e não se esforçam para economizar. Pelo contrário, é mais fácil para eles trabalhar com informações fragmentadas e duplicadas do que construir e manter abstrações complexas. É por isso que é mais fácil lidar com o código sem encapsulamento, com estruturas repetidas e severidade arquitetônica mínima.

Conclusão

Modelos de idiomas grandes são uma ferramenta útil no desenvolvimento, especialmente nos estágios iniciais ou ao criar um código auxiliar. Mas é importante adaptar a abordagem a eles: simplificar a arquitetura, limitar a abstração, evitar dependências complexas e não confiar nelas ao configurar o SDK.

Os princípios de sólidos, secos e limpos ainda são relevantes, mas dão o melhor efeito nas mãos de uma pessoa. Ao trabalhar com a LLM, é razoável usar um estilo prático e simplificado que permite obter um código confiável e compreensível que seja fácil de finalizar manualmente. E onde LLM esquece – a duplicação do código o ajuda a se lembrar.

Portando Surreal Engine C++ para WebAssembly

Neste post vou descrever como portei o motor de jogo Surreal Engine para WebAssembly.

Motor Surreal – um motor de jogo que implementa a maior parte das funcionalidades do Unreal Engine 1, jogos famosos neste motor – Torneio Unreal 99, Unreal, Deus Ex, Imortal. Refere-se a mecanismos clássicos que funcionavam principalmente em um ambiente de execução de thread único.

Inicialmente tive a ideia de assumir um projeto que não conseguiria concluir em um prazo razoável, mostrando assim aos meus seguidores do Twitch que existem projetos que nem eu consigo realizar. Durante minha primeira transmissão, de repente percebi que a tarefa de portar o Surreal Engine C++ para WebAssembly usando Emscripten é viável.

Surreal Engine Emscripten Demo

Depois de um mês posso demonstrar minha montagem de garfo e motor no WebAssembly:
https://demensdeum.com/demos/SurrealEngine/

O controle, como no original, é feito através das setas do teclado. Em seguida, pretendo adaptá-lo para controle móvel (tachi), adicionando iluminação correta e outros recursos gráficos da renderização do Unreal Tournament 99.

Por onde começar?

A primeira coisa que quero dizer é que qualquer projeto pode ser portado de C++ para WebAssembly usando Emscripten, a única dúvida é quão completa será a funcionalidade. Escolha um projeto cujas portas de biblioteca já estejam disponíveis para Emscripten, no caso do Surreal Engine, você tem muita sorte, pois o mecanismo usa as bibliotecas SDL 2, OpenAL – ambos foram portados para o Emscripten. No entanto, Vulkan é usado como uma API gráfica, que atualmente não está disponível para HTML5, o trabalho está em andamento para implementar WebGPU, mas também está em fase de rascunho, e também não se sabe quão simples será a porta adicional de Vulkan para WebGPU , depois de totalmente padronizado. Portanto, tive que escrever minha própria renderização básica OpenGL-ES/WebGL para Surreal Engine.

Construindo o projeto

Construir sistema no Surreal Engine – CMake, que também simplifica a portabilidade, porque Emscripten fornece aos seus construtores nativos – emcmake, emmake.
O porte do Surreal Engine foi baseado no código do meu último jogo em WebGL/OpenGL ES e C++ chamado Death-Mask, por isso o desenvolvimento foi muito mais simples, eu tinha todos os build flags necessários comigo e exemplos de código.

Um dos pontos mais importantes em CMakeLists.txt são os sinalizadores de construção do Emscripten. Abaixo está um exemplo do arquivo do projeto:


-s MAX_WEBGL_VERSION=2 \

-s EXCEPTION_DEBUG \

-fexceptions \

--preload-file UnrealTournament/ \

--preload-file SurrealEngine.pk3 \

--bind \

--use-preload-plugins \

-Wall \

-Wextra \

-Werror=return-type \

-s USE_SDL=2 \

-s ASSERTIONS=1 \

-w \

-g4 \

-s DISABLE_EXCEPTION_CATCHING=0 \

-O3 \

--no-heap-copy \

-s ALLOW_MEMORY_GROWTH=1 \

-s EXIT_RUNTIME=1")

O próprio script de construção:


emmake make -j 16

cp SurrealEngine.data /srv/http/SurrealEngine/SurrealEngine.data

cp SurrealEngine.js /srv/http/SurrealEngine/SurrealEngine.js

cp SurrealEngine.wasm /srv/http/SurrealEngine/SurrealEngine.wasm

cp ../buildScripts/Emscripten/index.html /srv/http/SurrealEngine/index.html

cp ../buildScripts/Emscripten/background.png /srv/http/SurrealEngine/background.png

Em seguida, prepararemos o índice .html , que inclui o pré-carregador do sistema de arquivos do projeto. Para fazer upload para a web, usei o Unreal Tournament Demo versão 338. Como você pode ver no arquivo CMake, a pasta do jogo descompactada foi adicionada ao diretório de construção e vinculada como um arquivo de pré-carregamento para Emscripten.

Alterações no código principal

Então foi necessário alterar o loop do jogo, você não pode executar um loop infinito, isso faz com que o navegador congele, em vez disso você precisa usar emscripten_set_main_loop, escrevi sobre esse recurso em minha nota de 2017 “< a href="https://demensdeum.com /blog/ru/2017/03/29/porting-sdl-c-game-to-html5-emscripten/" rel="noopener" target="_blank">Portar jogo SDL C++ para HTML5 (Emscripten)”
Alteramos o código para sair do loop while para if, então exibimos a classe principal do mecanismo de jogo, que contém o loop do jogo, no escopo global, e escrevemos uma função global que chamará a etapa do loop do jogo do objeto global :


#include <emscripten.h>

Engine *EMSCRIPTEN_GLOBAL_GAME_ENGINE = nullptr;

void emscripten_game_loop_step() {

	EMSCRIPTEN_GLOBAL_GAME_ENGINE->Run();

}

#endif

Depois disso, você precisa ter certeza de que não há threads em segundo plano no aplicativo, se houver, então prepare-se para reescrevê-los para execução de thread único ou use a biblioteca phtread no Emscripten.
O thread de segundo plano no Surreal Engine é usado para reproduzir música, os dados vêm do thread do mecanismo principal sobre a faixa atual, a necessidade de tocar música ou sua ausência, então o thread de segundo plano recebe um novo estado por meio de um mutex e começa a tocar nova música ou pausa-o. O fluxo de fundo também é usado para armazenar música em buffer durante a reprodução.
Minhas tentativas de construir o Surreal Engine para Emscripten com pthread não tiveram sucesso, porque as portas SDL2 e OpenAL foram construídas sem suporte a pthread e eu não queria reconstruí-las por causa da música. Portanto, transferi a funcionalidade do fluxo de música de fundo para execução de thread único usando um loop. Ao remover as chamadas pthread do código C++, movi o buffer e a reprodução da música para o thread principal, para que não houvesse atrasos, aumentei o buffer em alguns segundos.

A seguir, descreverei implementações específicas de gráficos e som.

Vulkan não é compatível!

Sim, Vulkan não é compatível com HTML5, embora todos os folhetos de marketing apresentem suporte multiplataforma e ampla plataforma como a principal vantagem do Vulkan. Por esse motivo, tive que escrever meu próprio renderizador gráfico básico para um tipo OpenGL simplificado – – ES, é usado em dispositivos móveis, às vezes não contém os recursos modernos do OpenGL moderno, mas porta muito bem para WebGL, que é exatamente o que o Emscripten implementa. A escrita da renderização básica de blocos, renderização bsp, para a exibição da GUI mais simples e renderização de modelos + mapas foi concluída em duas semanas. Esta foi talvez a parte mais difícil do projeto. Ainda há muito trabalho pela frente para implementar todas as funcionalidades da renderização do Surreal Engine, portanto, qualquer ajuda dos leitores é bem-vinda na forma de código e solicitações pull.

OpenAL compatível!

Grande sorte é que o Surreal Engine usa OpenAL para saída de áudio. Depois de escrever um hello world simples em OpenAL e montá-lo em WebAssembly usando Emscripten, ficou claro para mim como tudo era simples e comecei a portar o som.
Após várias horas de depuração, ficou óbvio que a implementação OpenAL do Emscripten possui vários bugs, por exemplo, ao inicializar a leitura do número de canais mono, o método retornou um número infinito, e após tentar inicializar um vetor de tamanho infinito, C++ trava com a exceção vector::length_error.

Conseguimos contornar isso codificando o número de canais mono para 2048:


		alcGetIntegerv(alDevice, ALC_STEREO_SOURCES, 1, &stereoSources);



#if __EMSCRIPTEN__

		monoSources = 2048; // for some reason Emscripten's OpenAL gives infinite monoSources count, bug?

#endif



Existe uma rede?

O Surreal Engine atualmente não suporta jogos online, jogar com bots é compatível, mas precisamos de alguém para escrever IA para esses bots. Teoricamente, você pode implementar um jogo em rede no WebAssembly/Emscripten usando Websockets.

Conclusão

Concluindo, gostaria de dizer que a portabilidade do Surreal Engine acabou sendo bastante tranquila devido ao uso de bibliotecas para as quais existem portas Emscripten, bem como à minha experiência anterior na implementação de um jogo em C++ para WebAssembly em Emscripten. Abaixo estão links para fontes de conhecimento e repositórios sobre o tema.
M-M-M-MATANÇA DE MONSTRO!

Além disso, se você quiser ajudar o projeto, de preferência com código de renderização WebGL/OpenGL ES, escreva para mim no Telegram:
https://t.me/demenscave

Links

https://demensdeum.com/demos/SurrealEngine/
https://github.com/demensdeum/SurrealEngine-Emscripten

https://github.com/dpjudas/SurrealEngine

Flash Forever – Interceptor 2021

Recently, it turned out that Adobe Flash works quite stably under Wine. During a 4-hour stream, I made the game Interceptor 2021, which is a sequel to the game Interceptor 2020, written for the ZX Spectrum.

For those who are not in the know – the Flash technology provided interactivity on the web from 2000 to around 2015. Its shutdown was prompted by an open letter from Steve Jobs, in which he wrote that Flash should be consigned to history because it lagged on the iPhone. Since then, JS has become even more sluggish than Flash, and Flash itself has been wrapped in JS, making it possible to run it on anything thanks to the Ruffle player.

You can play it here:
https://demensdeum.com/demos/Interceptor2021

Video:
https://www.youtube.com/watch?v=-3b5PkBvHQk

Source code:
https://github.com/demensdeum/Interceptor-2021

Repositório CRUD

Nesta nota descreverei os princípios básicos do conhecido padrão clássico CRUD, implementado na linguagem Swift. Swift é uma linguagem aberta e multiplataforma disponível para Windows, Linux, macOS, iOS, Android.

Existem muitas soluções para abstrair o armazenamento de dados e a lógica do aplicativo. Uma dessas soluções é a abordagem CRUD, que é um acrônimo para C– Criar, R -Leia, U– Atualização, D– Excluir.
Normalmente, este princípio é implementado através da implementação de uma interface para o banco de dados, na qual os elementos são manipulados usando um identificador único, como o id. Uma interface é criada para cada letra CRUD – Criar(objeto, id), Ler(id), Atualizar(objeto, id), Excluir(objeto, id).
Se um objeto contém um id dentro de si, então o argumento id pode ser omitido dos métodos (Create, Update, Delete), já que todo o objeto é passado para lá junto com seu campo – eu ia. Mas para – A leitura requer id porque queremos obter um objeto do banco de dados por id.

Todos os nomes são fictícios

Vamos imaginar que um hipotético aplicativo AssistantAI foi criado usando o SDK gratuito do banco de dados EtherRelm, a integração era simples, a API era muito conveniente e, como resultado, o aplicativo foi lançado nos mercados.
De repente, o desenvolvedor do SDK EtherRelm decide torná-lo pago, fixando o preço em US$ 100 por ano por usuário do aplicativo.
O que? Sim! O que os desenvolvedores da AssistantAI devem fazer agora, pois já possuem 1 milhão de usuários ativos! Pagar US$ 100 milhões?
Em vez disso, é tomada a decisão de avaliar a transferência da aplicação para o banco de dados RootData nativo da plataforma, segundo os programadores, tal transferência levará cerca de seis meses, isso não leva em consideração a implementação de novos recursos na aplicação; Depois de pensar um pouco, foi tomada a decisão de remover o aplicativo dos mercados, reescrevê-lo em outro framework multiplataforma gratuito com um banco de dados BueMS integrado, isso resolverá o problema com o banco de dados pago + simplificará o desenvolvimento em outras plataformas.
Um ano depois, o aplicativo foi reescrito em BueMS, mas de repente o desenvolvedor do framework decide torná-lo pago. Acontece que a equipe caiu na mesma armadilha duas vezes. Se conseguirão sair na segunda vez é uma história completamente diferente.

Abstração para o resgate

Esses problemas poderiam ter sido evitados se os desenvolvedores tivessem usado uma abstração de interfaces dentro da aplicação. Aos três pilares da OOP – polimorfismo, encapsulamento, herança, não faz muito tempo eles adicionaram outro – abstração.
A abstração de dados permite descrever ideias e modelos em termos gerais, com um mínimo de detalhes, ao mesmo tempo em que é preciso o suficiente para implementar implementações específicas usadas para resolver problemas de negócios.
Como podemos abstrair a operação do banco de dados para que a lógica da aplicação não dependa dela? Usamos a abordagem CRUD!

Um diagrama UML CRUD simplificado tem esta aparência:

Exemplo com um banco de dados EtherRelm fictício:

Exemplo com um banco de dados SQLite real:

Como você já percebeu, ao trocar o banco de dados, apenas ele muda a interface CRUD com a qual a aplicação interage permanece inalterada. CRUD é uma implementação do padrão GoF – Adaptador, porque com ele adaptamos interfaces de aplicações a qualquer banco de dados e combinamos interfaces incompatíveis.
As palavras estão vazias, mostre-me o código
Para implementar abstrações em linguagens de programação, são usadas interfaces/protocolos/classes abstratas. Todos esses são fenômenos da mesma ordem, porém, durante as entrevistas você pode ser solicitado a nomear a diferença entre eles, eu pessoalmente acho que isso não faz muito sentido porque o único propósito de uso é implementar a abstração de dados, caso contrário é para testar a memória do entrevistado.
O CRUD é frequentemente implementado dentro da estrutura do padrão Repository, porém, o repositório pode ou não implementar a interface CRUD, tudo depende da engenhosidade do desenvolvedor.

Considere um código Swift bastante típico para o repositório da estrutura Book, trabalhando diretamente com o banco de dados UserDefaults:


struct Book: Codable {
	let title: String
	let author: String
}

class BookRepository {
	func save(book: Book) {
    		let record = try! JSONEncoder().encode(book)
    		UserDefaults.standard.set(record, forKey: book.title)
	}
    
	func get(bookWithTitle title: String) -> Book? {
    		guard let data = UserDefaults.standard.data(forKey: title) else { return nil }
    		let book = try! JSONDecoder().decode(Book.self, from: data)
    		return book
	}
    
	func delete(book: Book) {
    		UserDefaults.standard.removeObject(forKey: book.title)
	}
}

let book = Book(title: "Fear and Loathing in COBOL", author: "Sir Edsger ZX Spectrum")
let repository = BookRepository()
repository.save(book: book)
print(repository.get(bookWithTitle: book.title)!)
repository.delete(book: book)
guard repository.get(bookWithTitle: book.title) == nil else {
	print("Error: can't delete Book from repository!")
	exit(1)
}

O código acima parece simples, mas vamos contar o número de violações do princípio DRY (Do not Repeat Yourself) e a coerência do código:
Conectividade com o banco de dados UserDefaults
Conectividade com codificadores e decodificadores JSON – JSONEncoder, JSONDecoder
Conectado com a estrutura Book, mas precisamos de um repositório abstrato para não criar uma classe de repositório para cada estrutura que iremos armazenar no banco de dados (violação DRY)

Vejo esse tipo de código de repositório CRUD com bastante frequência, ele pode ser usado, mas o alto acoplamento e a duplicação de código levam ao fato de que, com o tempo, seu suporte se tornará muito complicado. Isso será especialmente perceptível ao tentar mudar para outro banco de dados, ou ao alterar a lógica interna de trabalho com o banco de dados em todos os repositórios criados na aplicação.
Em vez de duplicar o código, mantenha o acoplamento alto – Vamos escrever um protocolo para o repositório CRUD, abstraindo assim a interface do banco de dados e a lógica de negócio da aplicação, respeitando o DRY, implementando baixo acoplamento:

    typealias Item = Codable
    typealias ItemIdentifier = String
    
    func create<T: CRUDRepository.Item>(id: CRUDRepository.ItemIdentifier, item: T) async throws
    func read<T: CRUDRepository.Item>(id: CRUDRepository.ItemIdentifier) async throws -> T
    func update<T: CRUDRepository.Item>(id: CRUDRepository.ItemIdentifier, item: T) async throws
    func delete(id: CRUDRepository.ItemIdentifier) async throws
}

O protocolo CRUDRepository descreve interfaces e tipos de dados associados para implementação adicional de um repositório CRUD específico.

A seguir escreveremos uma implementação específica para o banco de dados UserDefaults:

    private typealias RecordIdentifier = String
    
    let tableName: String
    let dataTransformer: DataTransformer
    
    init(
   	 tableName: String = "",
   	 dataTransformer: DataTransformer = JSONDataTransformer()
    ) {
   	 self.tableName = tableName
   	 self.dataTransformer = dataTransformer
    }
    
    private func key(id: CRUDRepository.ItemIdentifier) -> RecordIdentifier {
   	 "database_\(tableName)_item_\(id)"
    }
   	 
    private func isExists(id: CRUDRepository.ItemIdentifier) async throws -> Bool {
   	 UserDefaults.standard.data(forKey: key(id: id)) != nil
    }
    
    func create<T: CRUDRepository.Item>(id: CRUDRepository.ItemIdentifier, item: T) async throws {
   	 let data = try await dataTransformer.encode(item)
   	 UserDefaults.standard.set(data, forKey: key(id: id))
   	 UserDefaults.standard.synchronize()
    }
    
    func read<T: CRUDRepository.Item>(id: CRUDRepository.ItemIdentifier) async throws -> T {
   	 guard let data = UserDefaults.standard.data(forKey: key(id: id)) else {
   		 throw CRUDRepositoryError.recordNotFound(id: id)
   	 }
   	 let item: T = try await dataTransformer.decode(data: data)
   	 return item
    }
    
    func update<T: CRUDRepository.Item>(id: CRUDRepository.ItemIdentifier, item: T) async throws {
   	 guard try await isExists(id: id) else {
   		 throw CRUDRepositoryError.recordNotFound(id: id)
   	 }
   	 let data = try await dataTransformer.encode(item)
   	 UserDefaults.standard.set(data, forKey: key(id: id))
   	 UserDefaults.standard.synchronize()
    }
    
    func delete(id: CRUDRepository.ItemIdentifier) async throws {
   	 guard try await isExists(id: id) else {
   		 throw CRUDRepositoryError.recordNotFound(id: id)
   	 }
   	 UserDefaults.standard.removeObject(forKey: key(id: id))
   	 UserDefaults.standard.synchronize()
    }
}

O código parece longo, mas contém uma implementação concreta completa de um repositório CRUD contendo acoplamento fraco, detalhes abaixo.
typealias foram adicionadas para autodocumentação do código.
Acoplamento fraco e acoplamento forte
O descolamento de uma estrutura específica (struct) é implementado usando o T genérico, que por sua vez deve implementar os protocolos Codable. Codable permite converter estruturas usando classes que implementam os protocolos TopLevelEncoder e TopLevelDecoder, por exemplo JSONEncoder e JSONDecoder, ao usar tipos básicos (Int, String, Float, etc.) não há necessidade de escrever código adicional para converter estruturas.

A dissociação de um codificador e decodificador específico ocorre usando abstração no protocolo DataTransformer:

	func encode<T: Encodable>(_ object: T) async throws -> Data
	func decode<T: Decodable>(data: Data) async throws -> T
}

Usando a implementação de um transformador de dados, implementamos uma abstração das interfaces do codificador e do decodificador, implementando acoplamento fraco para garantir o trabalho com diferentes tipos de formatos de dados.

A seguir está o código para um DataTransformer específico, ou seja, para JSON:

	func encode<T>(_ object: T) async throws -> Data where T : Encodable {
    		let data = try JSONEncoder().encode(object)
    		return data
	}
    
	func decode<T>(data: Data) async throws -> T where T : Decodable {
    		let item: T = try JSONDecoder().decode(T.self, from: data)
    		return item
	}
}

Foi possível?

O que mudou? Agora basta inicializar um repositório específico para funcionar com qualquer estrutura que implemente o protocolo Codable, eliminando assim a necessidade de duplicação de código e implementação de acoplamento fraco da aplicação.

Um exemplo de cliente CRUD com um repositório específico, UserDefaults é o banco de dados, formato de dados JSON, estrutura do cliente, também um exemplo de escrita e leitura de um array:


print("One item access example")

do {
	let clientRecordIdentifier = "client"
	let clientOne = Client(name: "Chill Client")
	let repository = UserDefaultsRepository(
    	tableName: "Clients Database",
    	dataTransformer: JSONDataTransformer()
	)
	try await repository.create(id: clientRecordIdentifier, item: clientOne)
	var clientRecord: Client = try await repository.read(id: clientRecordIdentifier)
	print("Client Name: \(clientRecord.name)")
	clientRecord.name = "Busy Client"
	try await repository.update(id: clientRecordIdentifier, item: clientRecord)
	let updatedClient: Client = try await repository.read(id: clientRecordIdentifier)
	print("Updated Client Name: \(updatedClient.name)")
	try await repository.delete(id: clientRecordIdentifier)
	let removedClientRecord: Client = try await repository.read(id: clientRecordIdentifier)
	print(removedClientRecord)
}
catch {
	print(error.localizedDescription)
}

print("Array access example")

let clientArrayRecordIdentifier = "clientArray"
let clientOne = Client(name: "Chill Client")
let repository = UserDefaultsRepository(
	tableName: "Clients Database",
	dataTransformer: JSONDataTransformer()
)
let array = [clientOne]
try await repository.create(id: clientArrayRecordIdentifier, item: array)
let savedArray: [Client] = try await repository.read(id: clientArrayRecordIdentifier)
print(savedArray.first!)

Durante a primeira verificação CRUD, foi implementado o tratamento de exceções, em que a leitura do item remoto não estará mais disponível.

Trocar bancos de dados

Agora mostrarei como transferir seu código atual para outro banco de dados. Por exemplo, pegarei o código do repositório SQLite que o ChatGPT gerou:


class SQLiteRepository: CRUDRepository {
    private typealias RecordIdentifier = String
    
    let tableName: String
    let dataTransformer: DataTransformer
    private var db: OpaquePointer?

    init(
   	 tableName: String,
   	 dataTransformer: DataTransformer = JSONDataTransformer()
    ) {
   	 self.tableName = tableName
   	 self.dataTransformer = dataTransformer
   	 self.db = openDatabase()
   	 createTableIfNeeded()
    }
    
    private func openDatabase() -> OpaquePointer? {
   	 var db: OpaquePointer? = nil
   	 let fileURL = try! FileManager.default
   		 .url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
   		 .appendingPathComponent("\(tableName).sqlite")
   	 if sqlite3_open(fileURL.path, &db) != SQLITE_OK {
   		 print("error opening database")
   		 return nil
   	 }
   	 return db
    }
    
    private func createTableIfNeeded() {
   	 let createTableString = """
   	 CREATE TABLE IF NOT EXISTS \(tableName) (
   	 id TEXT PRIMARY KEY NOT NULL,
   	 data BLOB NOT NULL
   	 );
   	 """
   	 var createTableStatement: OpaquePointer? = nil
   	 if sqlite3_prepare_v2(db, createTableString, -1, &createTableStatement, nil) == SQLITE_OK {
   		 if sqlite3_step(createTableStatement) == SQLITE_DONE {
       		 print("\(tableName) table created.")
   		 } else {
       		 print("\(tableName) table could not be created.")
   		 }
   	 } else {
   		 print("CREATE TABLE statement could not be prepared.")
   	 }
   	 sqlite3_finalize(createTableStatement)
    }
    
    private func isExists(id: CRUDRepository.ItemIdentifier) async throws -> Bool {
   	 let queryStatementString = "SELECT data FROM \(tableName) WHERE id = ?;"
   	 var queryStatement: OpaquePointer? = nil
   	 if sqlite3_prepare_v2(db, queryStatementString, -1, &queryStatement, nil) == SQLITE_OK {
   		 sqlite3_bind_text(queryStatement, 1, id, -1, nil)
   		 if sqlite3_step(queryStatement) == SQLITE_ROW {
       		 sqlite3_finalize(queryStatement)
       		 return true
   		 } else {
       		 sqlite3_finalize(queryStatement)
       		 return false
   		 }
   	 } else {
   		 print("SELECT statement could not be prepared.")
   		 throw CRUDRepositoryError.databaseError
   	 }
    }
    
    func create<T: CRUDRepository.Item>(id: CRUDRepository.ItemIdentifier, item: T) async throws {
   	 let insertStatementString = "INSERT INTO \(tableName) (id, data) VALUES (?, ?);"
   	 var insertStatement: OpaquePointer? = nil
   	 if sqlite3_prepare_v2(db, insertStatementString, -1, &insertStatement, nil) == SQLITE_OK {
   		 let data = try await dataTransformer.encode(item)
   		 sqlite3_bind_text(insertStatement, 1, id, -1, nil)
   		 sqlite3_bind_blob(insertStatement, 2, (data as NSData).bytes, Int32(data.count), nil)
   		 if sqlite3_step(insertStatement) == SQLITE_DONE {
       		 print("Successfully inserted row.")
   		 } else {
       		 print("Could not insert row.")
       		 throw CRUDRepositoryError.databaseError
   		 }
   	 } else {
   		 print("INSERT statement could not be prepared.")
   		 throw CRUDRepositoryError.databaseError
   	 }
   	 sqlite3_finalize(insertStatement)
    }
    
    func read<T: CRUDRepository.Item>(id: CRUDRepository.ItemIdentifier) async throws -> T {
   	 let queryStatementString = "SELECT data FROM \(tableName) WHERE id = ?;"
   	 var queryStatement: OpaquePointer? = nil
   	 var item: T?
   	 if sqlite3_prepare_v2(db, queryStatementString, -1, &queryStatement, nil) == SQLITE_OK {
   		 sqlite3_bind_text(queryStatement, 1, id, -1, nil)
   		 if sqlite3_step(queryStatement) == SQLITE_ROW {
       		 let queryResultCol1 = sqlite3_column_blob(queryStatement, 0)
       		 let queryResultCol1Length = sqlite3_column_bytes(queryStatement, 0)
       		 let data = Data(bytes: queryResultCol1, count: Int(queryResultCol1Length))
       		 item = try await dataTransformer.decode(data: data)
   		 } else {
       		 throw CRUDRepositoryError.recordNotFound(id: id)
   		 }
   	 } else {
   		 print("SELECT statement could not be prepared")
   		 throw CRUDRepositoryError.databaseError
   	 }
   	 sqlite3_finalize(queryStatement)
   	 return item!
    }
    
    func update<T: CRUDRepository.Item>(id: CRUDRepository.ItemIdentifier, item: T) async throws {
   	 guard try await isExists(id: id) else {
   		 throw CRUDRepositoryError.recordNotFound(id: id)
   	 }
   	 let updateStatementString = "UPDATE \(tableName) SET data = ? WHERE id = ?;"
   	 var updateStatement: OpaquePointer? = nil
   	 if sqlite3_prepare_v2(db, updateStatementString, -1, &updateStatement, nil) == SQLITE_OK {
   		 let data = try await dataTransformer.encode(item)
   		 sqlite3_bind_blob(updateStatement, 1, (data as NSData).bytes, Int32(data.count), nil)
   		 sqlite3_bind_text(updateStatement, 2, id, -1, nil)
   		 if sqlite3_step(updateStatement) == SQLITE_DONE {
       		 print("Successfully updated row.")
   		 } else {
       		 print("Could not update row.")
       		 throw CRUDRepositoryError.databaseError
   		 }
   	 } else {
   		 print("UPDATE statement could not be prepared.")
   		 throw CRUDRepositoryError.databaseError
   	 }
   	 sqlite3_finalize(updateStatement)
    }
    
    func delete(id: CRUDRepository.ItemIdentifier) async throws {
   	 guard try await isExists(id: id) else {
   		 throw CRUDRepositoryError.recordNotFound(id: id)
   	 }
   	 let deleteStatementString = "DELETE FROM \(tableName) WHERE id = ?;"
   	 var deleteStatement: OpaquePointer? = nil
   	 if sqlite3_prepare_v2(db, deleteStatementString, -1, &deleteStatement, nil) == SQLITE_OK {
   		 sqlite3_bind_text(deleteStatement, 1, id, -1, nil)
   		 if sqlite3_step(deleteStatement) == SQLITE_DONE {
       		 print("Successfully deleted row.")
   		 } else {
       		 print("Could not delete row.")
       		 throw CRUDRepositoryError.databaseError
   		 }
   	 } else {
   		 print("DELETE statement could not be prepared.")
   		 throw CRUDRepositoryError.databaseError
   	 }
   	 sqlite3_finalize(deleteStatement)
    }
}

Ou o código CRUD do repositório do sistema de arquivos, que também foi gerado pelo ChatGPT:


class FileSystemRepository: CRUDRepository {
	private typealias RecordIdentifier = String
    
	let directoryName: String
	let dataTransformer: DataTransformer
	private let fileManager = FileManager.default
	private var directoryURL: URL
    
	init(
    	directoryName: String = "Database",
    	dataTransformer: DataTransformer = JSONDataTransformer()
	) {
    	self.directoryName = directoryName
    	self.dataTransformer = dataTransformer
   	 
    	let paths = fileManager.urls(for: .documentDirectory, in: .userDomainMask)
    	directoryURL = paths.first!.appendingPathComponent(directoryName)
   	 
    	if !fileManager.fileExists(atPath: directoryURL.path) {
        	try? fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil)
    	}
	}
    
	private func fileURL(id: CRUDRepository.ItemIdentifier) -> URL {
    	return directoryURL.appendingPathComponent("item_\(id).json")
	}
    
	private func isExists(id: CRUDRepository.ItemIdentifier) async throws -> Bool {
    	return fileManager.fileExists(atPath: fileURL(id: id).path)
	}
    
	func create<T: CRUDRepository.Item>(id: CRUDRepository.ItemIdentifier, item: T) async throws {
    	let data = try await dataTransformer.encode(item)
    	let url = fileURL(id: id)
    	try data.write(to: url)
	}
    
	func read<T: CRUDRepository.Item>(id: CRUDRepository.ItemIdentifier) async throws -> T {
    	let url = fileURL(id: id)
    	guard let data = fileManager.contents(atPath: url.path) else {
        	throw CRUDRepositoryError.recordNotFound(id: id)
    	}
    	let item: T = try await dataTransformer.decode(data: data)
    	return item
	}
    
	func update<T: CRUDRepository.Item>(id: CRUDRepository.ItemIdentifier, item: T) async throws {
    	guard try await isExists(id: id) else {
        	throw CRUDRepositoryError.recordNotFound(id: id)
    	}
    	let data = try await dataTransformer.encode(item)
    	let url = fileURL(id: id)
    	try data.write(to: url)
	}
    
	func delete(id: CRUDRepository.ItemIdentifier) async throws {
    	guard try await isExists(id: id) else {
        	throw CRUDRepositoryError.recordNotFound(id: id)
    	}
    	let url = fileURL(id: id)
    	try fileManager.removeItem(at: url)
	}
}

Substitua o repositório no código do cliente:


print("One item access example")

do {
	let clientRecordIdentifier = "client"
	let clientOne = Client(name: "Chill Client")
	let repository = FileSystemRepository(
    	directoryName: "Clients Database",
    	dataTransformer: JSONDataTransformer()
	)
	try await repository.create(id: clientRecordIdentifier, item: clientOne)
	var clientRecord: Client = try await repository.read(id: clientRecordIdentifier)
	print("Client Name: \(clientRecord.name)")
	clientRecord.name = "Busy Client"
	try await repository.update(id: clientRecordIdentifier, item: clientRecord)
	let updatedClient: Client = try await repository.read(id: clientRecordIdentifier)
	print("Updated Client Name: \(updatedClient.name)")
	try await repository.delete(id: clientRecordIdentifier)
	let removedClientRecord: Client = try await repository.read(id: clientRecordIdentifier)
	print(removedClientRecord)
}
catch {
	print(error.localizedDescription)
}

print("Array access example")

let clientArrayRecordIdentifier = "clientArray"
let clientOne = Client(name: "Chill Client")
let repository = FileSystemRepository(
	directoryName: "Clients Database",
	dataTransformer: JSONDataTransformer()
)
let array = [clientOne]
try await repository.create(id: clientArrayRecordIdentifier, item: array)
let savedArray: [Client] = try await repository.read(id: clientArrayRecordIdentifier)
print(savedArray.first!)

A inicialização de UserDefaultsRepository foi substituída por FileSystemRepository, com os argumentos apropriados.
Após executar a segunda versão do código do cliente, você encontrará um diretório “Banco de Dados de Clientes” na pasta de documentos, que conterá um arquivo de um array serializado em JSON com uma estrutura de Cliente.

Alterar formato de armazenamento de dados

Agora vamos pedir ao ChatGPT para gerar um codificador e decodificador para XML:

	let formatExtension = "xml"
    
	func encode<T: Encodable>(_ item: T) async throws -> Data {
    	let encoder = PropertyListEncoder()
    	encoder.outputFormat = .xml
    	return try encoder.encode(item)
	}
    
	func decode<T: Decodable>(data: Data) async throws -> T {
    	let decoder = PropertyListDecoder()
    	return try decoder.decode(T.self, from: data)
	}
}

Graças aos tipos integrados no Swift, a tarefa de uma rede neural torna-se elementar.

Substitua JSON por XML no código do cliente:


print("One item access example")

do {
	let clientRecordIdentifier = "client"
	let clientOne = Client(name: "Chill Client")
	let repository = FileSystemRepository(
    	directoryName: "Clients Database",
    	dataTransformer: XMLDataTransformer()
	)
	try await repository.create(id: clientRecordIdentifier, item: clientOne)
	var clientRecord: Client = try await repository.read(id: clientRecordIdentifier)
	print("Client Name: \(clientRecord.name)")
	clientRecord.name = "Busy Client"
	try await repository.update(id: clientRecordIdentifier, item: clientRecord)
	let updatedClient: Client = try await repository.read(id: clientRecordIdentifier)
	print("Updated Client Name: \(updatedClient.name)")
	try await repository.delete(id: clientRecordIdentifier)
	let removedClientRecord: Client = try await repository.read(id: clientRecordIdentifier)
	print(removedClientRecord)
}
catch {
	print(error.localizedDescription)
}

print("Array access example")

let clientArrayRecordIdentifier = "clientArray"
let clientOne = Client(name: "Chill Client")
let repository = FileSystemRepository(
	directoryName: "Clients Database",
	dataTransformer: XMLDataTransformer()
)
let array = [clientOne]
try await repository.create(id: clientArrayRecordIdentifier, item: array)
let savedArray: [Client] = try await repository.read(id: clientArrayRecordIdentifier)
print(savedArray.first!)

O código do cliente mudou para apenas uma expressão JSONDataTransformer -> XMLDataTransformer

Total

Repositórios CRUD são um dos padrões de design que podem ser usados ​​para implementar acoplamento fraco de componentes de arquitetura de aplicativos. Mais uma das soluções – usando ORM (Mapeamento Objeto-Relacional), resumindo, ORM usa uma abordagem em que as estruturas são completamente mapeadas para o banco de dados, e então as alterações com modelos devem ser exibidas (mapeadas(!)) no banco de dados.
Mas essa é uma história completamente diferente.

Uma implementação completa dos repositórios CRUD para Swift está disponível em:
https://gitlab.com/demensdeum/crud-example

A propósito, o Swift tem suporte fora do macOS há muito tempo; o código do artigo foi totalmente escrito e testado no Arch Linux.

Fontes

https://developer.apple.com/documentation/combine/topleveldecoder
https://developer.apple.com/documentation/combine/toplevelencoder
https://en.wikipedia.org/wiki/Create,_read,_update_and_delete

dd erro de entrada/saída

O que você deve fazer se receber um erro de entrada/saída ao copiar um disco normal usando dd no Linux?

Situevina é muito triste, mas tem solução. Provavelmente você está lidando com um disco com falha contendo blocos defeituosos que não podem mais ser usados, gravados ou lidos.

Certifique-se de verificar esse disco usando S.M.A.R.T., provavelmente ele mostrará erros no disco. Este foi o caso no meu caso; o número de blocos defeituosos foi tão grande que tive que dizer adeus ao disco rígido antigo e substituí-lo por um novo SSD.

O problema era que este disco continha um sistema totalmente funcional com software licenciado necessário para o trabalho. Tentei usar o partimage para copiar dados rapidamente, mas de repente descobri que o utilitário copia apenas um terço do disco e termina com um segfault ou com alguma outra piada engraçada do Sishny/Sipplusplus.

Em seguida, tentei copiar os dados usando dd, e descobri que dd chega aproximadamente ao mesmo lugar que partimage e então ocorre um erro de entrada/saída. Ao mesmo tempo, todos os tipos de sinalizadores engraçados como conv=noerr, skip ou qualquer outra coisa não ajudaram em nada.

Mas os dados foram copiados para outro disco sem problemas usando um utilitário GNU chamado ddrescue.

После этого мои волосы стали шелковистыми, вернулась жена, дети и собака перестала кусать диван.

Большим плюсом ddrescue является наличие встроенного прогрессбара, поэтому не приходится костылять какие-то ухищрения навроде pv и всяких не особо красивых флажков dd. Также ddrescure показывает количество попыток прочитать данные; еще на вики написано что утилита обладает каким-то сверх алгоритмом для считывания поврежденных данных, оставим это на проверку людям которые любят ковыряться в исходниках, мы же не из этих да?

https://ru.wikipedia.org/wiki/Ddrescue
https://www.gnu.org/software/ddrescue/ddrescue_ru.html