C ++: metaprogramação com uma API do compilador em vez de com recursos do C ++

10

Isso começou como uma pergunta SO, mas percebi que não é convencional e, com base na descrição real nos sites, pode ser mais adequado para programadores.se, já que a pergunta tem muito peso conceitual.

Eu tenho aprendido o LibTooling e é uma ferramenta muito poderosa capaz de expor todo o "detalhe" do código de uma maneira amigável, ou seja, de uma maneira semântica , e também sem adivinhar. Se o clang puder compilar seu código, o clang estará certo sobre a semântica de cada caractere único dentro desse código.

Agora permita-me recuar por um momento.

Existem muitos problemas práticos que surgem quando alguém se engaja na metaprogramação de modelos C ++ (e especialmente quando se aventura além dos modelos no território de macros inteligentes, embora aterradoras). Para ser honesto, para muitos programadores, inclusive eu, muitos dos usos comuns de modelos também são um pouco assustadores.

Eu acho que um bom exemplo seria strings em tempo de compilação . Essa é uma pergunta que já tem mais de um ano, mas está claro que o C ++ a partir de agora não facilita isso para meros mortais. Embora analisar essas opções não seja suficiente para causar náusea para mim, ainda assim, não me sinto confiante sobre a capacidade de produzir código de máquina mágico e maximamente eficiente para se adequar a qualquer aplicativo sofisticado que eu tenha para o meu software.

Quero dizer, vamos ser sinceros, pessoal, as cordas são bem simples e básicas. Alguns de nós querem apenas uma maneira conveniente de emitir código de máquina que tenha certas seqüências "incorporadas" significativamente mais do que conseguimos ao codificá-lo da maneira direta. No nosso código C ++.

Digite clang e LibTooling, que expõe a árvore de sintaxe abstrata (AST) do código-fonte e permite que um aplicativo C ++ simples e simples manipule correta e confiavelmente o código-fonte bruto (usando Rewriter) ao lado de um rico modelo semântico orientado a objetos de tudo no AST. Ele lida com muitas coisas. Ele conhece as expansões de macro e permite seguir essas cadeias. Sim, estou falando sobre transformação ou tradução de código fonte a fonte.

Minha tese fundamental aqui é que o clang agora nos permite criar executáveis ​​que podem funcionar como os estágios de pré-processador personalizados ideais para o nosso software C ++, e podemos implementar esses estágios de metaprogramação com C ++. Estamos simplesmente constrangidos pelo fato de que esse estágio deve receber uma entrada que seja um código C ++ válido e produzir como saída um código C ++ mais válido. Além disso, quaisquer outras restrições aplicadas pelo seu sistema de compilação.

A entrada deve ser pelo menos muito próxima do código C ++ válido, porque, afinal, clang é o front-end do compilador e estamos apenas bisbilhotando e sendo criativos com sua API. Não sei se existe alguma disposição para poder definir nova sintaxe a ser usada, mas claramente precisamos desenvolver maneiras de analisá-la adequadamente e adicioná-la ao projeto clang para fazer isso. Esperar mais é ter algo no projeto clang que está fora do escopo.

Não é um problema. Eu imagino que algumas funções macro não operacionais possam lidar com essa tarefa.

Outra maneira de analisar o que estou descrevendo é implementar construções de metaprogramação usando o C ++ em tempo de execução, manipulando o AST do nosso código-fonte (graças ao clang e sua API) em vez de implementá-los usando as ferramentas mais limitadas disponíveis na própria linguagem. Isso também traz benefícios claros no desempenho da compilação (cabeçalhos pesados ​​de modelo diminuem a compilação proporcionalmente à frequência com que você os utiliza. Muitas coisas compiladas são cuidadosamente correspondidas e jogadas fora pelo vinculador).

Isso, no entanto, tem o custo de introduzir uma ou duas etapas adicionais no processo de compilação e também no requisito de escrever algum software (reconhecidamente) um pouco mais detalhado (mas pelo menos é o C ++ em tempo de execução simples) como parte de nossa ferramenta .

Essa não é a imagem toda. Estou bastante certo de que existe um espaço muito maior de funcionalidade na geração de código extremamente difícil ou impossível com os principais recursos da linguagem. No C ++, você pode escrever um modelo ou macro ou uma combinação maluca de ambos, mas em uma ferramenta clang, é possível modificar classes e funções de QUALQUER maneira que você possa obter com o C ++, em tempo de execução , enquanto tem acesso total ao conteúdo semântico, além de modelo e macros e tudo mais.

Então, eu estou me perguntando por que todo mundo já não está fazendo isso. Será que essa funcionalidade do clang é tão nova e ninguém está familiarizado com a enorme hierarquia de classes do AST do clang? Não pode ser isso.

Talvez eu esteja subestimando um pouco a dificuldade disso, mas fazer "manipulação de string em tempo de compilação" com uma ferramenta clang é quase criminalmente simples. É detalhado, mas é incrivelmente simples. Tudo o que é necessário é um monte de funções macro não operacionais que são mapeadas para std::stringoperações reais reais . O plug-in clang implementa isso buscando todas as chamadas de macro não operacionais relevantes e executa as operações com seqüências de caracteres. Essa ferramenta é inserida como parte do processo de compilação. Durante a compilação, essas chamadas de função de macro não operacional são avaliadas automaticamente em seus resultados e, em seguida, inseridas novamente como strings simples em tempo de compilação no programa. O programa pode ser compilado como de costume. De fato, este programa resultante também é muito mais portátil como resultado, não exigindo um novo compilador sofisticado que suporte C ++ 11.

Steven Lu
fonte
Esta é uma pergunta invulgarmente longa. Você poderia condensá-lo nos seus pontos mais relevantes?
amon
Eu posto muitas perguntas longas. Mas especialmente com este, todas as partes da pergunta são importantes, eu acho. Talvez pule os 6 primeiros parágrafos? Haha
Steven Lu
3
Parece muito com as macros sintáticas pioneiras no Lisp e recentemente adotadas pelo Haxe, Nemerle, Scala e idiomas semelhantes. Há muita leitura sobre por que as macros Lisp são consideradas prejudiciais. Embora ainda não tenha ouvido um argumento convincente, você pode encontrar razões pelas quais as pessoas relutam em adicioná-los a todos os idiomas (além do fato de que não é necessariamente direto).
set2
Sim, é o seu C ++ meta-ifying. O que pode significar código melhor e mais rápido. Quanto a esses idiomas. Bem, por onde devo começar. O que é um videogame multimilionário implementado em qualquer um desses idiomas? O que é um navegador moderno implementado em qualquer um desses idiomas? Kernel do sistema operacional? Tudo bem, parece que Haxe tem alguma tração, mas você entendeu.
Steven Lu
11
@ NWP, Bem, não posso deixar de salientar que você parece ter perdido todo o ponto do post. As strings em tempo de compilação são simplesmente um exemplo concreto e minucioso dos recursos disponíveis para nós agora.
Steven Lu

Respostas:

7

Sim, Virginia, existe um Papai Noel.

A noção de uso de programas para modificar programas existe há muito tempo. A idéia original veio de John von Neumann na forma de computadores de programas armazenados. Mas modificar o código da máquina de maneira arbitrária é bastante inconveniente.

As pessoas geralmente querem modificar o código fonte . Isso é realizado principalmente na forma de sistemas de transformação de programas (PTS) .

O PTS geralmente oferece, para pelo menos uma linguagem de programação, a capacidade de analisar ASTs, manipular esse AST e regenerar texto fonte válido. Se, de fato, você procura, para a maioria das linguagens populares, alguém criou essa ferramenta (Clang é um exemplo para C ++, o compilador Java oferece esse recurso como uma API, a Microsoft oferece a Rosyln, o JDT do Eclipse, ...) com um procedimento. API que é realmente bastante útil. Para a comunidade mais ampla, quase toda comunidade específica de idioma pode apontar para algo assim, implementado com vários níveis de maturidade (geralmente modesto, muitos "apenas analisadores que produzem ASTs"). Feliz metaprogramação.

[Há uma comunidade orientada para a reflexão que tenta fazer metaprogramming de dentro da linguagem de programação, mas apenas alcançar "runtime" comportamento modifiation, e apenas na medida em que os compiladores de linguagem feitas algumas informações disponíveis pela reflexão. Com exceção do LISP, sempre há detalhes sobre o programa que não estão disponíveis por reflexão ("Luke, você precisa da fonte") que sempre limitam o que a reflexão pode fazer.]

O PTS mais interessante faz isso em linguagens arbitrárias (você fornece à ferramenta uma descrição da linguagem como parâmetro de configuração, incluindo no mínimo o BNF). Esse PTS também permite fazer a transformação "fonte a fonte", por exemplo, especificar padrões diretamente usando a sintaxe de superfície do idioma de destino; usando esses padrões, você pode codificar fragmentos de interesse e / ou encontrar e substituir fragmentos de código. Isso é muito mais conveniente do que a API de programação, porque você não precisa conhecer todos os detalhes microscópicos dos ASTs para realizar a maior parte do seu trabalho. Pense nisso como meta-metaprogramação: -}

Uma desvantagem: a menos que o PTS ofereça vários tipos de análises estáticas úteis (tabelas de símbolos, controle e análises de fluxo de dados), é difícil escrever transformações realmente interessantes dessa maneira, porque você precisa verificar tipos e verificar fluxos de informações para as tarefas mais práticas. Infelizmente, essa capacidade é de fato rara no PTS geral. (Ele está sempre indisponível com o sempre proposto "Se eu tivesse apenas um analisador ..." Veja minha biografia para uma discussão mais longa sobre "A vida após a análise").

Existe um teorema que diz que se você pode reescrever strings [portanto reescrever árvores], pode fazer uma transformação arbitrária; e, portanto, vários PTS se apoiam nisso para afirmar que você pode metaprogramar qualquer coisa com apenas as reescritas em árvore que elas oferecem. Enquanto o teorema é satisfatório no sentido de que agora você tem certeza de que pode fazer qualquer coisa, é insatisfatório da mesma maneira que a capacidade de uma máquina de Turing de fazer qualquer coisa não faz da programação de uma máquina de Turing o método de escolha. (O mesmo vale para sistemas com APIs apenas procedurais, se eles permitirem que você faça alterações arbitrárias no AST [e, de fato, acho que isso não é verdade para Clang]).

O que você deseja é o melhor dos dois mundos, um sistema que ofereça a generalidade do tipo de PTS parametrizado por idioma (mesmo manipulando vários idiomas), com análises estáticas adicionais, a capacidade de misturar transformações fonte a fonte com procedimentos APIs. Eu sei apenas de dois que fazem isso:

  • Linguagem de metaprogramação de patife (MPL)
  • nosso DMS Software Reengineering Toolkit

A menos que você queira escrever as descrições da linguagem e os analisadores estáticos (para C ++, isso é uma tremenda quantidade de trabalho, e é por isso que o Clang foi construído como um compilador e como uma metaprogramação de procedimentos geral), você desejará um PTS com descrições de linguagem maduras Já disponível. Caso contrário, você gastará todo o seu tempo configurando o PTS, e nenhum fazendo o trabalho que realmente queria. [Se você escolher um idioma aleatório e não mainstream, é muito difícil evitar essa etapa].

O Rascal tenta fazer isso cooptando o "OPP" (Analisador de Outras Pessoas), mas isso não ajuda na parte da análise estática. Eu acho que eles têm Java muito bem na mão, mas tenho certeza que eles não usam C ou C ++. Mas, é uma ferramenta de pesquisa acadêmica; difícil culpá-los.

Enfatizo que nossa ferramenta DMS [comercial] possui Java, C, C ++ front-ends completos disponíveis. Para C ++, ele abrange quase tudo no C ++ 14 para GCC e até as variações da Microsoft (e estamos aperfeiçoando agora), expansão de macros e gerenciamento condicional e controle no nível de método e análise de fluxo de dados. E sim, você pode especificar alterações gramaticais de maneira prática; criamos um sistema VectorC ++ personalizado para um cliente que estendeu radicalmente o C ++ para usar o valor das operações da matriz paralela de dados F90 / APL. O DMS foi usado para realizar outras tarefas de metaprogramação em grandes sistemas C ++ (por exemplo, remodelagem da arquitetura do aplicativo). (Eu sou o arquiteto por trás do DMS).

Feliz meta-metaprogramação.

Ira Baxter
fonte
Legal, acho que o Clang e o DMS, embora tenham alguns recursos sobrepostos, são peças de software que não estão realmente na mesma categoria. Quero dizer, um é provavelmente ridiculamente caro e eu provavelmente nunca poderia justificar os recursos necessários para obter acesso a ele, e o outro é um código aberto gratuito e irrestrito. Essa é uma enorme diferença ... Parte do que me deixa empolgado com esses recursos empolgantes de metaprogramação é o fato de que é permitido não apenas usá-lo livremente, mas também distribuir livremente ferramentas binárias baseadas em clang.
Steven Lu
Qualquer coisa vendida comercialmente é "ridiculamente cara" em comparação com a gratuita. O custo bruto não é o problema; o que importa é que, para algumas pessoas, o retorno da aquisição do produto comercial é maior do que o retorno do artefato livre; caso contrário, não haveria software comercial. Obviamente, isso depende de suas necessidades específicas. O clang é um ponto interessante no espaço da ferramenta e certamente terá pontos de aplicação útil. Eu gostaria de pensar (já que sou o arquiteto do DMS) que o DMS tem recursos mais amplos. É improvável que o Clang ofereça suporte a outros idiomas além do C ++, como exemplo.
Ira Baxter
Certamente. Não há dúvida de que o DMS é incrivelmente poderoso, quase ao ponto da mágica (à la Arthur C. Clarke), e embora o clang seja ótimo, é realmente apenas um front-end em C ++ bem escrito, dos quais existem muitos. Muitos pequenos passos adiante, ou seja, ainda não seria realmente justo compará-lo ao DMS. Infelizmente, mesmo com ferramentas tão poderosas à nossa disposição, o software de trabalho não se escreve. Ele ainda deve existir através de uma tradução cuidadosa usando as ferramentas ou (quase sempre a opção superior) escrita de novo.
Steven Lu
Você não pode se dar ao luxo de criar ferramentas como Clang ou DMS a partir de novas. Em geral, você não pode se dar ao luxo de lançar o aplicativo que escreveu com uma equipe de 10 anos ao longo de 5 anos. Vamos precisar dessas ferramentas com mais e mais frequência, à medida que o tamanho e a vida útil do software continuam a crescer.
Ira Baxter
@StevenLu: Bem, o DMS agradece pelo elogio, mas não há nada de mágico nisso. O DMS tem o benefício de quase duas décadas lineares de engenharia e uma plataforma de arquitetura limpa (aw, shucks, YMMV) que se manteve muito bem. Da mesma forma, Clang tem muita engenharia boa nele. Concordo que eles não foram projetados para resolver exatamente o mesmo problema ... O escopo do DMS é explicitamente planejado para ser maior quando se trata de manipulação simbólica de programa e muito menor quando se trata de ser um compilador de produção.
Ira Baxter
4

A metaprogramação em C ++ com a API dos compiladores (em vez de usar modelos) é realmente interessante e praticamente possível. Como a metaprogramação ainda não é padronizada, você estará vinculado a um compilador específico, o que não é o caso dos modelos.

Então, eu estou me perguntando por que todo mundo já não está fazendo isso. Será que essa funcionalidade do clang é tão nova e ninguém está familiarizado com a enorme hierarquia de classes do AST do clang? Não pode ser isso.

Muitas pessoas fazem isso, mas em outros idiomas. Minha opinião é que a maioria dos desenvolvedores de C ++ (ou Java ou C) não vê a necessidade (talvez com razão) ou não está acostumada a abordagens de metaprogramação; Também acho que eles estão satisfeitos com os recursos de refatoração / geração de código do IDE e que qualquer coisa mais sofisticada pode ser vista como muito complexa / difícil de manter / difícil de depurar. Sem ferramentas adequadas, pode ser verdade. Você também deve levar em consideração a inércia e outros problemas não técnicos, como a contratação e / ou treinamento de pessoas.

A propósito, como mencionamos o Common Lisp e seu sistema macro (veja a resposta do Basile), devo dizer que ontem, o Clasp foi lançado (não sou afiliado):

O Clasp pretende ser uma implementação Common Lisp em conformidade que seja compilada ao LLVM IR. Além disso, expõe as bibliotecas Clang (AST, Matcher) ao desenvolvedor.

  • Primeiro, isso significa que você pode escrever em CL e não usar mais C ++, exceto ao usar suas bibliotecas (e se precisar de macros, use macros de CL).

  • Em segundo lugar, você pode escrever ferramentas em CL para o seu código C ++ existente (análise, refatoração, ...).

coredump
fonte
3

Vários compiladores C ++ têm uma API mais ou menos documentada e estável, em particular a maioria dos compiladores de software livre.

O Clang / LLVM é principalmente um grande conjunto de bibliotecas, e você pode usá-las.

O GCC recente está aceitando plugins . Em particular, você pode estendê-lo usando MELT (que é em si um meta-plugin, fornecendo uma linguagem específica de domínio de nível superior para estender o GCC).

Observe que a sintaxe do C ++ não é facilmente extensível no GCC (e provavelmente nem no Clang), mas você pode adicionar seus próprios pragmas, buildins, atributos e passes do compilador para fazer o que você deseja (talvez também forneça algumas macros de pré-processador que invocam essas coisas para fornecer uma sintaxe amigável).

Você pode estar interessado em linguagens e compiladores de vários estágios, consulte, por exemplo, o artigo Implementando idiomas de vários estágios usando ASTs, Gensym e Reflexão de C.Calcagno et al. e contornar o MetaOcaml . Você certamente deve procurar dentro das instalações de macro do Common Lisp . E você poderia estar interessado em bibliotecas JIT como libjit , GNU relâmpago , mesmo LLVM , ou de tempo de execução simplesmente -pelo - gerar algum código C ++, desembolsar uma compilação de-lo em uma biblioteca de objetos dinâmica compartilhada, em seguida, dlopen (3) que partilhou objeto. O blog de J.Pitrat também está relacionado a essas abordagens reflexivas. E também RefPerSys .

Basile Starynkevitch
fonte
Interessante. É muito bom ver o GCC continuando a evoluir aqui. Esta não é uma resposta que aborda tudo o que eu pedi, mas eu gosto disso.
Steven Lu
Re: suas novas edições ... Esse é um bom argumento sobre a reescrita de código. Na verdade, isso também começa a trazer esse recurso de metaprograma para C ++, muito mais acessível do que antes, o que também é bastante interessante.
Steven Lu