Tag Archive | Qt

Estilos de multitasking cooperativo, loop de eventos e programação assíncrona

introUm dos assuntos que mais me interessa em programação é programação assíncrona. Esse é um tema ao qual eu fui introduzido desde que comecei a brincar com Qt (framework para criar interfaces gráficas) lá por volta de 2010, mas com o tempo fui tendo experiências em vários “paradigmas” de programação assíncrona, passando, por exemplo, por Node.js e Boost.Asio. O tema muito me interessa e foi, por exemplo, o motivador para eu estudar sistemas operacionais e arquitetura de computadores.

Esse é um trabalho que eu tive a ideia de fazer já faz muito tempo. Passou até ideia de, em vez de fazer um post, evoluir mais a ideia e transformar em contribuição e também a ideia de transformar em episódio de podcast. Acho que evolui muito durante esse tempo, para quem antes não entendia nem o porquê de uma troca de contexto ser lenta.

Motivação

Muitas vezes, nos deparamos com problemas que exigem o tratamento contínuo de várias tarefas, como tratar eventos de rede e manipular arquivos locais, por exemplo. Uma intuição ao encarar esse problema seria o uso de threads, já que há várias tarefas em “paralelo”. Entretanto, nem todos os trabalhos efetuados pelo computador são executados exclusivamente pela CPU.

Há outros componentes além da CPU, mas não programáveis e que não executam seu algoritmo, que costumam ser mais lentos e possuem outras tarefas, como transformar um sinal digital em analógico, por exemplo. Além disso, a comunicação com esses componentes ocorre de forma serial, através do ciclo buscar-decodificar-executar-checar-interrupção. Há uma simplificação nesse argumento, mas o fato é que você não lê dois arquivos em paralelo do mesmo HD. Em suma, usar threads não é uma abstração natural ao problema e só introduz gargalo desnecessário e complexo.

“Para quem só sabe usar martelo, tudo parece um prego.” — velho provérbio Klingon

Outro motivo para evitar o uso de threads como resposta ao problema é que logo você terá mais threads que CPUs/núcleos e irá se deparar com o que é conhecido como problema C10K. Mesmo que não fosse um gargalo absurdo, só o fato de você necessitar de mais threads que CPUs disponíves já torna o seu programa mais restrito, pois ele não mais poderá funcionar em um ambiente bare metal, sem a presença de um sistema operacional moderno ou um scheduler.

E o grande problema de desempenho que threads introduzem decorre do fato de elas exigirem uma troca de contexto com o kernel. É claro que esse não é o único problema, pois há também o custo de criação da thread. O programa estaria realizando a criação de threads, que podem acabar tendo um tempo de vida curto e, além disso, passarem a maior parte de seu tempo de vida dormindo. A própria criação da thread não é completamente escalável, pois requer a alocação de sua própria pilha, porém a memória é um recurso global e já aí temos um ponto de contenção.

O problema de desempenho de uma troca de contexto do sistema operacional lembra o problema de desempenho da convenção de chamada de funções usada por compiladores, mas pior.

Funções são unidades isoladas, encapsuladas, e de tal forma deveriam trabalhar. Ao realizar uma chamada de função, a função atual não tem conhecimento de quais registradores serão usados pela nova função. A função atual não detém a informação de quais registradores terão seus valores sobrescritos pela nova função. Assim, as convenções de chamadas de função exigem dois pontos de processamento extra, um para salvar os valores dos registradores na pilha, e outro para restaurar os valores. É por isso que alguns programadores são tão fissurados em fazer function inlining.

O problema de desempenho da troca de contexto é maior, porque ele deve salvar o valor de todos os registradores e ainda sofre com o gargalo do scheduler e da própria troca de contexto. E se processos estiverem envolvidos, então, ainda tem o gargalo de reconfigurar a unidade de gerenciamento de memória. Esse estilo de processamento multitarefa recebe o nome de multitarefa preemptivo. Você pode aprender mais sobre esses custos estudando arquitetura de computadores e sistemas operacionais.

É possível obter concorrência, que é a propriedade de executar várias tarefas no mesmo período de tempo, sem recorrer a paralelismo real. Para essas tarefas que estão mais atreladas a I/O, é interessante abandonarmos o paralelismo durante o tratamento de I/O para alcançarmos mais escalabilidade, evitando o problema do C10K. E já que um novo design se faz necessário, é bom levar em conta multitarefa cooperativo e obter um resultado até melhor do que o inicialmente planejado.

O loop de eventos

E uma abordagem para o problema de programação, que eu vejo sendo usada mais em jogos do que em qualquer outro lugar, é a abordagem de loop de eventos. Há essas peculiaridades de que em jogos você não manipula arquivos como se fosse um banco de dados, com ambiciosos requisitos de desempenho, e também de desenvolver o jogo para quando completar, não mudar o código nunca mais, sem qualquer compromisso com flexibilidade e manutenção. E é com essa abordagem, a abordagem do loop de eventos, que começamos.

Há uma biblioteca, a biblioteca de baixo nível SDL, que tem como objetivo ser apenas uma camada de abstração multimídia, para suprir o que já não é suprido pela própria especificação da linguagem C, focando no desenvolvedor de jogos. A SDL faz uso de um sistema de eventos para tratar da comunicação entre o processo e o mundo externo (teclado, mouse, janelas, …), que é comumente usada em algum loop que o programador prepara. Essa mesma estrutura é utilizada em vários outros locais, incluindo na Allegro, que foi o maior competidor da SDL, no passado.

A ideia é ter um conjunto de funções que faz a ponte da comunicação entre o processo e o mundo externo. No mundo SDL, eventos são descritos através do tipo não-extensível SDL_Event. Então você usa funções como a SDL_PollEvent para receber os eventos e funções dedicadas para iniciar operações que atuem no mundo externo. O suporte a programação assíncrona na biblioteca SDL é fraco, mas esse mesmo princípio de loop de eventos poderia ser usado em uma biblioteca que fornecesse um suporte mais forte a programação assíncrona. Abaixo você pode ver o exemplo de um código que faz uso de eventos da SDL:

Há as bibliotecas de interface gráfica e widgets, como a GTK+, a EFL e a Qt, que levam essa ideia um passo adiante, abstraindo o loop de eventos, antes escrito e reescrito por você, em um objeto. A Boost.Asio, que não é focada em interface gráficas, mas foca exclusivamente em operações assíncronas, possui uma classe de propósito similar, a classe io_service.

Para que você deixe de fazer o código boilerplate de rotear eventos a ações específicas, essas classes tratam essa parte para você, possivelmente através de callbacks, que é uma abstração antiga. A ideia é que você associe eventos a funções que podem estar interessadas em tratar tais eventos. Toda a lógica de rotear os eventos, agora, passa a fazer parte do objeto “loop de eventos”, em vez de um loop de eventos bruto. Esse estilo de programação assíncrona é um estilo do modelo passivo, pois você só registra os callbacks e cede o controle para a framework.

Agora que estamos em um nível de abstração superior ao do loop de eventos, vamos parar a discussão sobre loop de eventos. E esses objetos que estamos mencionando como objetos “loop de eventos”, serão mencionados, a partir de agora, como executors.

O executor

O executor é um objeto que pode executar unidades de trabalho encapsuladas. Utilizando somente C++11, podemos implementar um executor que gerencie o agendamento de operações relacionadas a espera de alguma duração de tempo. Há o recurso global RTC e, em vez de criar várias e várias threads para a execução de operações bloqueantes como a operação sleep_for, vamos usar um executor, que irá agendar e tratar todos os eventos de tempo que se façam necessário. Abaixo está o código para uma possível implementação de tal executor:

Esse código de exemplo me faz lembrar do sleepsort.

No exemplo, sem o uso de threads, foi possível realizar as várias tarefas concorrentes de espera de tempo. Para tal, ao objeto executor, foi dada a responsabilidade de compartilhar o recurso global RTC. Como a CPU é mais rápida que as tarefas requisitadas, uma única thread foi suficiente e, além disso, mesmo assim houve um período de tempo no qual o programa ficou ocioso.

Há alguns conceitos que já podem ser extraídos desse exemplo. Primeiro, vamos considerar que o executor seja uma abstração padrão, já fornecida de alguma forma e interoperável entre todos os códigos que façam uso de operações assíncronas. Quando o programa deseja fazer a operação assíncrona “esperar”, ele requisita o início da operação a alguma abstração (nesse caso é o próprio executor, mas é mais comum encontrar tais interações através de “objetos I/O”) através da função que inicia a operação. O controle é passado para o executor (através do método run), que continuamente verifica notificações de tarefas concluídas. Quando uma tarefa é concluída, o executor empresta o controle da thread para o código do usuário, executando a função que havia sido registrada para tratar a notificação de finalização da tarefa. Quando não há mais nenhuma tarefa na fila, o executor cede o controle da thread, por completo, para o código do usuário.

Como só existe uma thread de execução, mas várias tarefas a executar, nós temos o problema de compartilhamento de recursos, que nesse caso é a própria CPU/thread. O controle deveria ir e voltar entre alguma abstração e o código do usuário. E aí está o princípio do estilo multitarefa cooperativo. Há pontos de customização de comportamento entre os algoritmos responsáveis por executar as tarefas, fazendo com que eles cedam e emprestem o tempo da CPU para realização de outras tarefas.

O estilo de multitarefa cooperativo tem a vantagem de que os pontos de paradas são bem definidos, pois você sabe exatamente quando o controle passa de uma tarefa a outra. Então não há aquele grande gargalo que vimos com trocas de contextos, onde todos os registradores precisam ser salvos, entre outros. Uma solução mais elegante, eficiente e verde.

O objeto que passamos como último argumento da função add_sleep_for_callback é um callback, também conhecido como completion handler. Reflita sobre o que aconteceria se uma nova operação de espera fosse requisitada dentro de um dos callbacks que registramos. Abaixo há uma versão evoluída do executor que implementamos.

Esse detalhe de implementação me lembra a variável de SHELL SHLVL.

Um caso interessante é o da linguagem JavaScript, que possui um tipo de “executor implícito”, que passa a funcionar quando chega ao fim do seu código. Nesse caso, você não precisa escrever códigos como “while (true) executor.run_one()” ou “executor.run()“, mas apenas registrar os callbacks e se assegurar que não há nenhum loop infinito que impeça que o controle passe ao executor.

Introduzida a motivação, o texto passou a prosseguir reduzindo menções a I/O, por motivos de simplicidade e foco, mas tenha em mente que usamos operações assíncronas, majoritariamente, para realizar interações com o mundo externo. Então muitas das operações são agendadas condicionalmente em resposta a notificação de término de uma tarefa anterior (ex.: se protocolo inválido, feche o socket, do contrário, agende mais uma operação de leitura). Propostas como a N3785 e a N4046 definem executors também para gerenciar thread pools, não somente timeouts dentro de uma única thread. E por último, também é possível implementar executors que agendem a execução de operações de I/O dentro de uma mesma thread.

Algoritmos assíncronos descritos de forma síncrona

O problema com essa abordagem de callbacks é que antes, possuíamos código limpo e legível. O código podia ser lido sequencialmente, pois é isso que o código era, uma sequência de instruções. Entretanto, agora precisamos espalhar a lógica entre múltiplos e múltiplos callbacks. Agora você passa a ter blocos de código relacionados longe uns dos outros. Lambdas ajudam um pouco com o problema, mas não o suficiente. O problema é conhecido como callback/nesting hell e é similar ao problema do espaguete. Não sendo o bastante, o fluxo de execução do código se tornou controvertido pela própria natureza assíncrona das operações e construções como estruturas de condição e repetição e até mesmo código de tratamento de erro ficam representáveis de formas longe do ideal, obscuras e difíceis de ler.

Uma abstração de procedimentos muito importante para programação assíncrona é a corotina. Existe a abstração de procedimentos a qual nos referimos por função, que é uma implementação do conceito de subrotina. E temos a corotina, que é uma generalização do conceito de subrotina. A corotina possui duas operações a mais que a subrotina, sendo elas suspender e resumir.

Quando sua função é uma corotina (possível quando a linguagem dá suporte a corotinas), ela pode suspender antes de chegar ao final de sua execução, possivelmente devolvendo um valor durante essa ação de suspender. Um exemplo de onde corotinas são úteis é na implementação de uma suposta função geradora fibonacci e um programa que use essa função para imprimir os dez primeiros números dessa sequência infinita. O código Python abaixo demonstra uma implementação de tal exemplo, onde se percebe a elegância, legibilidade e reaproveitamento que o conceito de corotina permite quando temos o problema de multitarefa cooperativa:

Esse código aí me lembra das funções setjmp/longjmp.

Uma característica a qual se deve dar atenção caso você não esteja familiarizado com o conceito é que o valor das variáveis locais a função fibonacci foi preservado entre as várias “chamadas”. Mais precisamente, quando a função era resumida, ela possuía a mesma pilha de execução de quando foi suspensa. Essa é a uma implementação “completa” do conceito de corotina, uma corotina com pilha. Há também implementações de corotinas sem pilha de execução, onde somente o “número da linha de código” que estava executando é restaurado.

A proposta N4286 introduz uma nova palavra-chave, await, para identificar um ponto para suspender uma corotina. Fazendo uso de tal funcionalidade, é apresentado o seguinte exemplo, que elegantemente define um algoritmo assíncrono, descrito, na minha humilde opinião, de forma bastante síncrona, e fazendo uso das várias construções da linguagem para controle de fluxo de execução (estrutura de seleção, repetição…).

Corotinas resolvem o problema de complexidade que algoritmos assíncronos demandam. Entretanto, há várias propostas para implementação de corotinas e nenhuma delas foi padronizada ainda. Um caso interessante é o da biblioteca Asio, que, usando macros e um mecanismo similar ao Duff’s device, dá suporte a corotinas que não guardem uma pilha de execução. Dentre todo o trabalho que está sendo investido, o que eu espero que aconteça na comunidade de C++ é que a padronização siga o princípio de “você só paga pelo que usa” e que a especificação permita implementações bem performáticas.

Enquanto a padronização de corotinas não acontece e ficamos sem a solução a nível de linguagem, podemos optar por soluções a nível de biblioteca. Como C++ é uma linguagem que suporta abstrações de baixo nível, você consegue acesso a várias facilidades que podem ser usadas para implementar suporte a corotinas, tipo setjmp e longjmp e até mais coisas indo para o mundo não-portável fora da especificação. Uma biblioteca que parece bem promissora e que espero ver sendo incluída na Boost esse ano é a biblioteca Fiber, que imita a API de threads a qual estamos acostumados para fornecer “threads” agendadas cooperativamente, em espaço de usuário. A biblioteca usa o conceito de fibra, análogo ao conceito de thread, e em uma thread, você pode executar várias fibras. Tal biblioteca fornece a expressividade que precisamos para escrever algoritmos assíncronos de forma síncrona.

Outra solução enquanto você não pode esperar, é não usar corotinas, pois ainda será possível conseguir performance excelente através das técnicas comentadas ao longo do texto. O grande porém vai ser o fluxo embaralhado do código-fonte (ofuscação de código, para quê te quero?).

Completion tokens

E nesse período de tempo incerto quanto a que solução tornará-se o padrão para programação assíncrona na linguagem C++, a Boost.Asio, desde a versão 1.54, adotou um princípio interessante, de implementar uma solução extensível. Tal modelo extensível é muito bem documentado na proposta N4045 e aqui nessa seção há somente um resumo do que está contido em tal proposta.

A proposta é que, dado que o modelo de callbacks é confuso, ele seja evoluído para suportar outros modelos. Então, em vez da função receber como argumento um completion handler (o callback), ela deve receber um completion token, que é a solução de customização/extensibilidade proposta.

A proposta N4045 usa uma abordagem top-down, de primeiro mostrar como a proposta é usada, para depois mostrar como ela é implementada. Abaixo há um código de exemplo retirado da proposta:

No código de exemplo anterior, cada vez que a variável yield é passada a alguma operação assíncrona (ex.: open e read), a função é suspensa até que a operação seja concluída. Quando a operação é concluída, a função é resumida no ponto em que foi suspensa e a função que iniciou a operação assíncrona retorna o resultado da operação. A biblioteca Fiber, mencionada anteriormente, fornece um yield_context para o modelo extensível da Asio, o boost::fibers::asio::yield. Código assíncrono escrito de uma forma síncrona. Entretanto, um modelo extensível é adotado, pois não sabemos qual será o padrão para operações assíncronas, então não podemos forçar o uso da biblioteca Fiber goela abaixo.

Para tornar o modelo extensível, o tipo do retorno da função precisa ser deduzido (a partir do token) e o valor do retorno da função também precisa ser deduzido (também a partir do token). O tipo do retorno da função é deduzido a partir do tipo do token e o valor retornado é criado a partir do objeto token passado como argumento. E você ainda tem o handler, que deve ser chamado quando a operação for concluída. O handler também é extraído a partir do token. O modelo de completion tokens faz uso de type traits para extrair todas essas informações e, caso tais traits não sejam especializados, o comportamento padrão é tratar o token como um handler, tornando a abordagem retrocompatível com o modelo de callbacks.

Vários exemplos de token são dados no documento N4045:

  • use_future
  • boost::fibers::asio::use_future
  • boost::fibers::asio::yield
  • block

A abordagem de usar std::future tem impactos significativos na performance e não é uma abstração legal, como o próprio N4045 explica em suas seções iniciais, então vamos evitá-la. É até por isso que eu nem comentei sobre ela até então nesse texto.

Sinais e slots

Uma alternativa que foi proposta ao modelo de callbacks é a abordagem de “signals & slots”. Essa abordagem é implementada na libsigc++, na Boost, na Qt e em várias outras bibliotecas.

A proposta de usar sinais introduz esse conceito, de sinal, que é usado para notificar algum evento, mas abstrai o processo de entregar as notificações e registrar as funções que tratem o evento. O código que notifica os eventos só precisa se preocupar em emitir o sinal toda vez que o evento acontecer, pois o próprio sinal vai cuidar do conjunto de slots, que são as porções de código a serem executadas para tratar os eventos.

Essa abordagem costuma permitir um grande desacoplamento, em oposição a abordagem verbosa muito usada em Java. Um efeito interessante, também, a depender da implementação, é que você pode conectar um sinal a outro sinal, para evitar o trabalho de escrever você próprio o código que sincronize a emissão de um sinal a emissão de outro sinal. É possível também ter muitos sinais conectados a um único slot, assim como um único sinal conectado a múltiplos slots.

O sinal costuma ser associado a um objeto, e quando tal objeto é destruído, as conexões que haviam sido feitas também o são. Assim como também é possível ter slots como métodos de um objeto, que são desconectados de todos os sinais tão logo o objeto é destruído.

Como os sinais são abstrações independentes e operantes assim que se tornam expostos, é natural ser incentivado a remover o argumento de callback das funções que iniciam operações assíncronas, pois haveria duplicação de esforços. Se você for até mais longe e remover também a própria função que inicie a operação assíncrona, expondo ao usuário apenas os sinais para receber as notificações, sua framework deixará de seguir o modelo ativo para seguir o modelo passivo. Exemplos de tais modelos passivos é o socket da biblioteca Qt, que não possui uma função explícita para iniciar a operação de leitura, e a biblioteca POCO, que não possui uma função explícita para iniciar o recebimento de uma requisição HTTP.

Outro detalhe que temos no caso da ideia de sinais, é o controle de acesso. No caso da biblioteca Qt, sinais são implementados de uma forma que exige a cooperação de um pré-processador, o executor da própria Qt e a classe QObject. No caso da biblioteca Qt, a emissão de sinais segue as regras de controle de acesso de métodos protegidos em C++, onde todas as classes-filha podem realizar a emissão de sinais declaradas nas classes-pai. Enquanto a operação de conectar um sinal a outro sinal ou a um slot segue as mesmas regras de membros públicos, onde todo mundo pode realizar.

No caso das bibliotecas que implementam o conceito de sinais como um tipo, é comum ver um tipo sinal que englobe tanto a operação de emitir o sinal, quanto a operação de conectar o sinal a algum slot (diferente do que vemos na proposta de futures e promises, onde é possível ter um controle de acesso separado para as diferentes responsabilidades/operações/entidades).

A abordagem de sinais é legal, mas ela não resolve o problema de complexidade que é resolvido por corotinas. Eu descrevi essa abordagem com o intuito de facilitar a explicação sobre a diferença entre os modelos ativo e passivo.

Modelo ativo versus passivo

No modelo passivo, você não agenda o início das operações e, apesar de ser mais comum em frameworks de produtividade, há muitas perguntas que esse modelo não responde bem (isso é só uma opinião e não um fato), que acabam exigindo o projeto de bem mais abstrações para contornar o problema.

Fazendo um contraste rápido entre a biblioteca Qt e a Boost.Asio. Em ambas as bibliotecas, você possui classes para abstraírem o conceito de socket, mas, enquanto na Qt você trata o evento readyRead e usa o método readAll para receber o buffer com o conteúdo, na Boost.Asio você inicia a operação async_read_some e passa o buffer a ser utilizado como argumento. A Qt usa o modelo passivo e a Boost.Asio usa o modelo ativo.

O evento readyRead, da Qt, age independente do usuário e requer a alocação do buffer toda vez que ocorre. Como, então, você responde a perguntas como “como eu posso customizar o algoritmo de alocação do buffer?”, “como eu posso customizar a aplicação para fazer reaproveitamento de buffers?”, “como eu faço para usar um buffer pré-alocado na stack?” e outras. Como o modelo passivo não responde a perguntas como essa, você precisa inflar a abstração de socket com mais pontos de customização para que comportamentos como esses sejam possíveis. É uma explosão combinatória que acontece para cada abstração que lide com operações assíncronas. No modelo ativo, fica bem natural. Se há algum recurso que a operação que o programador está prestes a iniciar necessita, só precisa passar o recurso como argumento, pois no modelo ativo, o programador explicitamente inicia as operações. E não é só sobre customizar obtenção de recursos. Mais um exemplo de pergunta que o modelo passivo não responde bem é “como eu decido se vou ou não aceitar uma conexão (ou adiar para depois, durante cenários de sobrecarga do servidor)?”. É um grande poder para aplicações sérias que estão seriamente preocupadas com performance e necessitam de ajustes meticulosos.

Além da questão de performance e ajuste fino, o modelo passivo também é problemático para realizar depuração e testes, pois ele faz uma inversão de controle. Esse é um dos motivos pelos quais, até hoje, eu vejo frameworks e frameworks introduzindo race condition nos testes ao depender de timeout para garantir a “robustez” de suas implementações. Essa solução de timeout não resolve um problema que ocorre somente no modelo passivo, mas no modelo passivo, ele ocorre com muito mais frequência. No modelo ativo, seu callback pode ser chamado mesmo quando o erro acontece. No modelo passivo é tão problemático, que já vi eventos de erro serem declarados como notificações separadas, que podem ser facilmente ignoradas, característica indesejável quando você quer diminuir e evitar os bugs no seu código.

Graças ao modelo ativo, muito da biblioteca que estou desenvolvendo com o intuito de submeter a Boost foi simplificado.

O modelo passivo, no entanto, é ótimo para prototipações rápidas. Felizmente podemos implementar abstrações que introduzam o modelo passivo em termos do modelo ativo e ter o melhor (ou o pior, se você for um projetista de C#) dos dois mundos.

Referências bônus

Uns links aleatórios que também me ajudaram como referência ou podem servir de leitura aprofundada para quem quer mais:

Comentários bônus

Espero ter introduzido você, estimado leitor que não comenta, as principais práticas utilizadas no universo da programação assíncrona. E agora que você já está armado com uma visão geral da “área”, se jogar nesse mar da internet para buscar mais referências e cavar mais fundo é só o segundo passo.

Esses dois meses que não postei nada e fui perdendo acessos/visitantes… valeram a pena, pois estou bastante satisfeito com esse trabalho e esse blog é sobre minha satisfação pessoal, por isso que eu nem coloco propaganda.

Há alguns outros textos separados em minha pasta de rascunho com temas variando desde ideias originais até assuntos “manjados” que eu pretendo usar quando bater aquela preguiça de investir muito tempo para um post só.

Agora que eu finalmente discuti concorrência sem paralelismo nesse blog, eu devia atualizar meu notebook (ou corrigir meu desktop) para ter um hardware suficientemente bom e começar a estudar OpenCL para fazer uns posts sobre paralelismo massivo.

EDIT (2015/3/11):

  • Ressaltar sugestão de estudar arquitetura de computadores e sistemas operacionais na seção de motivação.
  • Menção sobre “executor implícito” que ocorre em JavaScript.
  • Adicionado exemplo de motivação não relacionado a buffers na discussão de modelo ativo versus modelo passivo.

Why is it a bad idea to rewrite Tufão build system under qmake

Since Tufão 0.4, I’ve been using CMake as the Tufão build system, but occasionally I see some users reimplementing the qmake-based project files and I thought it’d be a good idea to explain/document why such rewrite is a bad idea. This is the post.

Simply and clear (1 reason)

It means *nothing* to your qmake-based project.

What your qmake-based project needs is a *.pri file to you include in your *.pro file. And such *.pri file *is* generated (and properly included in your Qt-installation) by Tufão. You’ll just write the usual “CONFIG += TUFAO” without *any* pain.

Why won’t I use qmake in Tufão ever again (long answer)

Two reasons why is it a bad idea:

  • You define only one target per file. You need subdirs. It’s hard.
    • The Tufão unit testing based on Qt Test requires the definition of separate executables per test and the “src/tests/CMakeLists.txt” CMake file beautifully defines 13 tests. And with the CMake-based system, all you need to do to add a new test is add a single line to the previously mentioned file. QMake is so hard that I’d define a dumb test system that only works after you install the lib, just to free myself from the qmake-pain.
  • There is no easy way to preprocess files.
    • And if you use external commands that you don’t provide yourself like grep, sed or whatever, then your build system will be less portable than autotools. Not every Windows developer likes autotools and your approach (external commands) won’t be any better.
    • All in all, it becomes hard to write a build system that will install the files required by projects that use QMake, CMake or PKG-CONFIG (Tufão supports all three).

The reasons above are the important reasons, but there are others like the fact that documentation is almost as bad as the documentation for creation of QtCreator plugins.

The ever growing distance from QMake

When Tufão grows, sometimes the build system becomes more complicated/demanding, and when it happens, I bet the QMake-based approach will become even more difficult to maintain. The most recent case of libtufao.pro that I’m aware of had to include some underdocumented black magic like the following just to meet the Qt4/5 Tufão 0.x demand:

You like a CMake-based Tufão

The current CMake-based build system of Tufão provides features that you certainly enjoy. At least the greater stability of the new unit testing framework requires CMake and you certainly want a stable library.

QMake hate

In the beginning, QMake met Tufão requirements of a build system, but I wouldn’t use it again for demanding projects.

But I don’t hate QMake and I’d use it again in a Qt-based project if, and only if, I *know* it won’t have demanding needs. Of course I’ll hate QMake if people start to overuse it (and creating me trouble).

And if you still wants to maintain a QMake-based Tufão project file, at least you’ve been warned about the *pain* and inferior solution you’ll end up with.

Palestra sobre o Tufão no FISL14

Tive uma palestra aceita no FISL14 e o tema da palestra é a framework Tufão.

Espero mostrar rapidamente o histórico do projeto e usar o tempo restante para a motivação, reservando 10 minutos ao final para perguntas. A motivação deve incluir uma pequena explicação de como funciona a web e seus principais pontos, comparações, becnhmarks (esse é um item mais difícil), limitações, exemplos e um “tutorial”.

Devo preparar uma nova aplicação de exemplo que explora características presentes no Tufão.

Tufão 1.0.0

After a long time developing Tufão, it finally reached 1.0 version some hours ago. I’ve spent a lot of time cleaning up the API and exploring the features provided by C++11 and Qt5 to release this version.

This is the first Tufão release that:

  • … breaks config files, so you’ll need to update your config files to use them with the new version
  • … breaks ABI, so you’ll need to recompile your projects to use the new version
  • … breaks API, so you’ll need to change your source code to be able to recompile your previous code and use the new version
  • … breaks previous documented behaviour, so you’ll need to change the way you use features that were available before. But don’t worry, because the list of these changes are really small and are well documented below.

Porting to Tufão 1.0

From now on, you should link against tufao1 instead of tufao. The PKGCONFIG, qmake and CMake files were renamed also, so you can have different Tufão libraries in the same system if their major version differs.

The list of behavioural changes are:

  • Headers are being stored using a Hash-table, so you can’t easily predict (and shouldn’t) the order of the headers anymore. I hope this change will improve the performance.
  • HttpServerRequest::ready signal auto-disconnects all slots connected to the HttpServerRequest::data and HttpServerRequest::end slots before being emitted.
  • HttpFileServer can automatically detect the mime type from the served files, so if you had your own code logic to handle the mimes, you should throw it away and enjoy a lesser code base to maintain.

Most of the errors related to API changes are caught at compile-time and you can use the Qt5’s new connection syntax and C++11’s override keyword to catch the remaining errors.

News

The list of changes:

  • The project finally have a logo (made by me in Inkscape)
  • Deprecated API was removed
  • Url and QueryString removed in favor of QUrl
  • Headers refactored to inherit from QMultiHash instead of QMultiMap
  • HttpServerResponse
    • Constructor’s options argument is optional now
    • setOptions method added
    • Constructor takes a reference to a QIODevice instead a pointer
  • HttpServerRequest
    • Constructor takes a reference to a QAbstractSocket instead a pointer
    • socket method returns a reference instead a pointer
    • url returns a QUrl
    • data signal was changed and you must use readBody method to access body’s content
    • the upgrade’s head data is accessed from the request body from now on
    • now the object auto-disconnects slots from data and end signals right before emit ready
    • setCustomData and customData methods added
      • Now HttpServerRequestRouter use these methods to pass the list of captured texts
  • HttpServer uses reference instead of pointers in several places
  • AbstractHttpServerRequestRouter refactored to explore lambdas features
  • Tufão’s plugin system fully refactored
    • It’s using JSON files as configuration
  • AbstractHttpServerRequestHandler::handleRequest
    • It uses references instead pointers
    • It receives 2 arguments instead of 3
  • One more abstraction to sessions created to explore lambdas
  • WebSocket
    • startServerHandshake is taking references instead pointers
  • LESS POINTERS and MORE REFERENCES
    • This change exposes a model more predictive and natural
    • I’m caring less about Qt style and more about C++ style
      • But don’t worry, I’ll maintain a balance
  • Using scoped enums
  • HttpFileServer uses/sends mime info
  • Interfaces don’t inherit from QObject anymore, so you can use multiple inheritance to make the same class implement many interfaces
  • HttpUpgradeRouter introduced
    • HttpServer::setUpgradeHandler also
  • Updated QtCreator plugin to work with QtCreator 2.7.0 and Qt 5

The future

I want to improve the Tufão’s stability and performance, so now I’ll focus on a minor relase with unit testing and some minor changes.

After Tufão 1.0.1, I’ll focus on BlowThemAll.

Bonus

You can see a visualization video based on the history of commits below.

This release deserves a wallpaper, so I made one. See it below:

Tufão wallpaper

You can download the source of the previous wallpaper here.

What are you still doing here? Go download the new version right now!

JSON-RPC 2.0 sobre WebSockets com Tufão + Phobos-RPC (Qt)

Sou uma pessoa que simpatiza com jogos e, ultimamente, estive trabalhando em um jogo orientado a disputas multiplayer pertencente ao gênero Bomberman. Para implementar a arquitetura de jogo sugerida pelo time de desenvolvimento, desenvolvi algumas bibliotecas auxiliares. E nesse post vou documentar como combinar as duas bibliotecas para permitir chamadas de métodos remotas (RPC).

A primeira biblioteca que desenvolvi para o projeto foi batizada de Phobos e sua única função é implementar o protocolo JSON-RPC 2.0. Meu objetivo principal durante seu desenvolvimento foi auxiliar o desenvolvimento do BlowThemAll e, assim sendo, alguns fatores, como arquitetura e documentação, foram deixados de lado. O projeto não tem página própria na internet e as “releases” do mesmo encontram-se na página de downloads do BlowThemAll.

Há um segundo projeto que iniciei, batizado de Tufão. É uma biblioteca desenvolvida em cima do Qt que possui uma API similar ao Node.js que pode ser usada para criar aplicações web. Desenvolvi esse projeto de forma bem disciplinada, me dedicando bastante, seja ao projetar sua arquitetura, seja ao escrever sua documentação, seja durante os testes, empacotamento para diferentes plataformas, preparação de uma página decente, criação de exemplos e documentação por fora… Caso queira saber mais sobre o mesmo, visite a página do projeto.

Pré-requisitos

 Esse texto assume que você está utilizando (e tem um pouco de familiaridade com) o QtCreator para gerenciar seus projetos e tem uma boa noção de orientação a objetos. Além disso, esse texto assume que você já instalou o Tufão no seu sistema.

Preparando um novo projeto

A primeira coisa a fazer, é baixar a biblioteca Phobos e seu “adapter” para o Tufão.

Então, usando o plugin do Tufão para o QtCreator, crie um novo projeto usando o template “application“.

Com o projeto criado, extraia o conteúdo dos arquivos baixados para a pasta do projeto e edite o arquivo *.pro do mesmo, de forma que as seguintes linhas sejam adicionadas:

Pronto, você concluiu as etapas iniciais para trabalhar com um projeto usando JSON-RPC 2.0 em C++. A etapa seguinte dependerá se o projeto criado é o servidor ou o cliente.

Preparando um novo projeto (servidor)

Grande parte da “aplicação web” já está pronta, utilizando o “esqueleto” do projeto genérico criado pelo plugin do Tufão. As alterações que você precisa fazer se resumem a criar uma classe para tratar conexões WebSocket e uma classe Handler para ser utilizada pelo RPC.

Comece criando a classe WebServer, que deve herdar de Tufao::HttpServer e deve implementar o método protegido upgrade, que deve ficar assim:

Implemente a classe Handler herdando de QObject e utilizando a macro Q_INVOKABLE na frente da declaração/assinatura dos métodos que você deseja exportar para o cliente remoto.

O último passo é editar o arquivo main.cpp, para que o mesmo utilize a classe WebServer criada. Para isso, só precisa mudar o tipo da variável server dentro da função main, de Tufao::HttpServer para WebServer.

Preparando um novo projeto (cliente)

O cliente tem uma natureza bem diferente do esqueleto genérico de aplicação criado pelo plugin do Tufão e isso requer que você faça mais mudanças que no caso do servidor.

Primeiro, apague as classes (desnecessários para o projeto) NotFound e PluginReloader. Apague também o arquivo de recursos static.qrc e seu recurso associado, notfound.html.

Segundo, apague quase todo o conteúdo da função main, para que se resuma ao seguinte:

Com isso terminaram as remoções necessários do projeto-base. Agora restam as adições, que se dividem em duas partes, relacionadas ao Tufão e relacionadas ao Phobos. Irei comentar o mínimo possível sobre o Tufão, pois o mesmo está bem documentado e torna-se desnecessário que eu faça comentários extras.

Começando pela parte do Tufão, você precisará criar um objeto da classe Tufao::WebSocket e chamar o método connectToHost. Quando o sinal connected for emitido, o objeto estará pronto para ser utilizado.

Do lado do Phobos, você precisará, assim como no servidor, de um objeto Handler, que herde da classe QObject e utilize a macro Q_INVOKABLE na frente da declaração/assinatura dos métodos que você deseja exportar para o servidor remoto (o protocolo JSON-RPC não distingue servidor de cliente e, uma vez iniciada a conexão, ambos podem realizar chamadas remotas, enquanto a camada de transporte permitir).

Invocando métodos remotos

Para chamar métodos remotos, utilize o método JsonRpc::callWith. Os dois primeiros argumentos que ele recebe lembram o método singleShot da classe QTimer, com a diferença que a macro SLOT não cerca o argumento member no método callWith. Os argumentos restantes representam, respectivamente, o nome do método remoto e seus argumentos. Exemplificando:

Be happy

Você pode obter o código-fonte das aplicações desenvolvidas nesse texto aqui.

Understanding Tufão, part 1

This post is dedicated to Tufão, but before talk about Tufão, I’ll tell a small preparatory text. Once while surfing on the web, I found Node.js. What is Node.js? An excelent asynchronous JavaScript web API. Why is it excelent? It allows you write little code to do a lot of web-related things. There are lots of web frameworks in this level, but they usually don’t provide the right level of abstraction. Why not? They don’t support even old modern HTTP (the web protocol) like chunked entities and 100-continue, what about real modern HTTP features like WebSocket, WebRTC, among other then? You just can’t use modern features. You’ll have problems even with basic examples like doing live stream from a webcam. After this day, the day I knew a decent web API, I started to care about web development. And then, I created the Tufão project.

Tufão is a tool to work with web in C++. Why C++? I like C++ and this is enough to me, but there are other reasons to create a web framework for C++. First, computing is changing again. In the last years, the clock speed of the processors stalled and again, you need to care about performance. Not only that, but the nature of techniques to gain performance are changing too. Today, you find processor with more and more cores, you can see more general purpose enabled GPUs and there are more optimized instruction sets in processors. C++ is the right tool for the job, and with the language update in the last year, I believe that the language is becoming stronger. Second, I don’t see a de facto standard to create web applications using C++. Third, there are lots of good libraries and APIs in C++ that you can use to implement cool ideas and develop web apps in C++ would let you to use them.

So, instead of porting a not decent API to create a C++ web framework, I choose Node.js to create the API core. I believe that C++ programmers love controlness, so they will like the API’s power. Besides the core, I’m implementing other facilites to help in the use of application servers, file upload, forms parsing, WebSocket, WebRTC and other modern standards, web services, …

How does Tufão compare to other frameworks? In this post, I’ll compare it through some different points of view.

The programming language

My idea about programming language is that languages should be designed to express ideas, and if they don’t have a good expressive power, then they are useless. Today we have lots and lots of programming languages. Personally, I don’t like the direction the Java language is taking, but, on the other side, I like Python. I already see some languages created only to develop web applications. I don’t believe that this is a good move. I don’t believe that the web development’s problem is the lack of an appropriate language. There are lots of good languages available today and I don’t believe that create new languages is necessary.

I’m developing Tufão in C++. C++ has a good expressive power and the last year’s update improved it. The language isn’t popular among web development, but web development is very dynamic and change rapidly from time to time. How I said before, the problem, in my opinion, is not in the lack of an appropriate language, but in the lack of an appropriate framework.

Modules

Tufão can have extra modules as any other framework, but compared to Node.js, I see a problem with ease of the installation process. In Node.js, we have npm and all we need to do is npm install <module>, but in Tufão you need rely in the operating system installation process or compile the modules. This isn’t a problem in some GNU/Linux distributions like ArchLinux, but this might not be the case in other systems.

I don’t see an ease way to solve this problem, so I’ll try to evolve Tufão to be self-sufficient in most of the cases. And this isn’t really difficult as Tufão is built on top of Qt, that already provides lots and lots of common needed features to any application.

Routing mechanism

In Tufão, the rounting is built on top of two classes. The AbstractHttpServerRequestHandler interface and the HttpServerRequestRouter class. The AbstractHttpServerRequestHandler interface requires you to create a slot to handle a request and the HttpServerRequestRouter is itself is an AbstractHttpServerRequestHandler. This design let you to build things like nested routing and use the same router in several different combinations (serving to HTTP and HTTPS servers).

In Tufão, the basic building block for routing request is the same as several other frameworks. You just need to use regular expression to filter the path and pass a handler to that request, and, optionally, specify which methods the object will handle.

The plugin-based application server

Well, when you develop a web application, you want to do it available 24h per day, every day. Imagine how you’d feel if you had to stop the server, recompile the application and run it again just to add your company’s name at the bottom of the page.

We could just use a dynamic interpreted language and solve the problem, but the language isn’t the problem. In some web frameworks there is the notion of an application server, that usually is integrated in the routing mechanism. The role of the application server is to dynamically register new applications and remove old ones.

In C++, a dynamic handler is just a plugin stored in a shared library loaded at runtime by the application. It’s just the old *.dll files on Windows or *.so on Linux. Tufão provides an application server too, but, different from most of the frameworks I’ve seen, it’s not melted in the routing mechanism.

The Tufão application server is plugin-based, stores its configuration in a file (chosen by you) and provides an external program to update this file. It’s easy to use, but you’ll have to add code to define when the application server must reload the config file. If you don’t have any special requeriments, the default application template code is enough and you won’t need to write this piece of code at all.

MVC separation

Some frameworks just don’t separate logic and ui design, such as most of old PHP, embedding the logic code and the html (or some custom view language code) code in the same file.

There are frameworks that allow you separate the logic and the ui design, but do a wrong implementation of the idea, requiring the designer to have some knowledge about the framework internals, trapping you to use the tools from the same framework forever.

What is the idea of other frameworks? Other frameworks let you export application data to application ui design (through web services or other), allowing you to use any language to generate your HTML/CSS and import the data through JavaScript. This idea by alone don’t only help to lower the security problems (designers thinking about programming) and the application ugliness (programmers thinking about ui design), but also help to use HTTP features to decrease the network traffic needed by you application, making the use of cache more efficient (in HTTP level). How this happens? This is the old Ajax technique.

  1. Write ui design code in pure HTML, CSS and JavaScript and put the files in the public folder.
  2. Write logic to generate and serve the data in C++, without caring about ui design, and export the data in some format simple to import from JavaScript, like JSON.
  3. Write JavaScript code to import the data from the services and populate the views.

Using this technique, when the user access your site, the browser will request the pages server through the Tufão’s static file server, making most part of the page cacheable, saving precious network traffic from your application . Also, the view isn’t generated at runtime, so you will save server’s processing power too, increasing the maximum numbers of requests per second.

Another advantage is the freedom to choose technologies. The UI is just a bunch of static files, isn’t something chained to specific Tufão internals, so you can use any tool you want to create them. We don’t take away your freedom.

As an advice, you should implement each page handler as a plugin, so you can change the running code at runtime.

IDE

Web frameworks usually don’t develop custom IDEs and this isn’t an exception for Tufão. But, Tufão is built on top of Qt, and Qt programmers are used to QtCreator, so I created a QtCreator plugin to help the development of Tufão applications. The plugin just help to create new applications, providing application templates. I think this is enough, but if I change my mind, I’ll extend the plugin.

There are 3 application templates currently:

  • The first is a full featured web application, using the Tufão’s static file server, plugin server and a custom handler to treat 404-pages.
  • The second is a plugin that responds with a “Hello World” and is as small as possible to keep things simple.
  • The last is just a Hello World application.

Development time

Well, It’s not an easy task to measure the development productivity, but I can talk about some points. If what you want is a creative web app, you problably need a powerfull low-limited web API to help you, and my aim is to provide such API, so I highly invite you to test Tufão and give me some feedback.

If you plan to create some common-case application, there are frameworks such as Django that already provide several integrated facilities (such as mvc with integrated mapping of database schemas, template systems, already implemented authentacion sytems, … …) and right now, Tufão can only compete if you use other libraries to help you.

Improve Tufão to compete with Django isn’t my priority and I don’t even know if I care about facilitate the creation process of boring common projects, but is an important area and if the demand or the help increase, I’ll create tools to help in this use case too.

Framework’s future

Tufão is an open source project under the LGPL license. The documentation, examples and public headers are under the MIT license. This means that anyone can contribute to Tufão development and you can use it in your commercial projects.

I created Tufão all alone and tried to do I really good job in every piece of it, but maybe the documentation isn’t good enough, so if you have any question that cannot be solved reading the documentation, just ask me and in the next version the documentation might be better. The same rule applies to bugs (and I wrote lots of tests to prevent it, but I just didn’t realize how to automate all tests, like little endian and big endian tests…).

That’s it

In the next part of this article, I’ll demonstrate some benchmarks and how to create a web application using as much Tufão’s features as possible. Thank you for reading about the project, I really appreciate, and if you like the project, spread it among your friends.

I ackowledge Ryan Lienhart Dahl for developing Node.js. Before its API, I thought web development was tedious.

Tufão (the Qt/C++ web framework) 0.2 released

Hello good fellows, I’m not used to write posts in english, but this is a habit that I must change. Well, just today I’m releasing a new version of my C++/Qt web framework, Tufão.

For the newcomers, Tufão was, initially, a port of the excellent Node.js API to C++ built on top of Qt. It was a good choice because the Node.js assynchronous API fits well in Qt’s object communication system (signals & slots).

The new 0.2 version brings the following changes:

  • Code is more stable
  • Documentation a bit improved
  • HttpServerResponse class is easier to use (stream operator)
  • WebSocket support
  • Added TUFAO_VERSION_MAJOR and TUFAO_VERSION_MINOR macros
  • A QtCreator plugin
  • New examples

The most problematic areas of Tufão now is its lack of:

  • An application server
  • A file server

In future versions these gaps will be filled. For more details take a look at the project roadmap in the README file.

I welcome everyone to compile, test and use Tufão.

%d blogueiros gostam disto: