Clang e IDE: una historia sobre la amistad y la amistad

Para mí, comenzó hace seis años y medio, cuando, por voluntad del destino, me atrajeron a un proyecto cerrado. Cuyo proyecto - no preguntes, no lo diré. Solo puedo decir que su idea fue simple como un rastrillo: incrustar el front-end clang en el IDE. Bueno, como se hizo recientemente en QtCreator, en CLion (en cierto sentido), etc. Clang era entonces una estrella en ascenso, muchos estaban tratando de evitar la posibilidad de utilizar finalmente el analizador de C ++ completo de forma casi gratuita. Y la idea, por así decirlo, literalmente estaba en el aire (y el autocompletado del código integrado en la API de clang fue insinuado por Be), solo tenía que tomarlo y hacerlo. Pero, como dijo Boromir, "No puedes tomarlo y ...". Entonces sucedió en este caso. Para más detalles - Bienvenido bajo cat.


Primero sobre el bien


Los beneficios de usar clang como un analizador incorporado en el IDE C ++, por supuesto, son. Al final, las funciones IDE no se limitan únicamente a la edición de archivos. Esta es una base de datos de caracteres, tareas de navegación, dependencias y mucho más. Y aquí, un compilador completo se dirige a su altura máxima, porque dominar todo el poder del preprocesador y las plantillas en un analizador autoescrito relativamente simple no es una tarea trivial. Debido a que generalmente tiene que hacer muchos compromisos, lo que obviamente afecta la calidad del análisis de código. A quién le importa: puede ver, por ejemplo, el analizador incorporado de QtCeator aquí: analizador Qt Creator C ++


En el mismo lugar, en el código fuente de QtCreator, puede ver que lo anterior no es todo lo que el IDE requiere del analizador. Además, necesita al menos:


  • resaltado de sintaxis (léxico y semántico)
  • todo tipo de sugerencias "sobre la marcha" con la visualización de información en el símbolo
  • Consejos sobre lo que está mal con el código y cómo solucionarlo / complementarlo
  • Completar código en una amplia variedad de contextos
  • la refactorización más diversa

Por lo tanto, en los beneficios enumerados anteriormente (¡realmente serio!), Las ventajas terminan y comienza el dolor. Para comprender mejor este dolor, primero puede ver el informe de Anastasia Kazakova ( anastasiak2512 ) sobre lo que realmente se requiere del analizador de código integrado en el IDE:



La esencia del problema


Pero es simple, aunque puede no ser obvio a primera vista. En pocas palabras, entonces: clang es un compilador . Y se refiere al código como un compilador . Y agudizado por el hecho de que el código se le ha entregado ya completado, y no el trozo del archivo que ahora está abierto en el editor IDE. A los compiladores no les gustan los bits de archivos, como construcciones incompletas, identificadores escritos incorrectamente, retrun en lugar de return y otras delicias que pueden surgir aquí y ahora en el editor. Por supuesto, antes de la compilación, todo esto se limpiará, arreglará y pondrá en línea. Pero aquí y ahora, en el editor, es lo que es. Y es de esta forma que el analizador integrado en el IDE llega a la tabla cada 5-10 segundos. Y si la versión autoescrita del mismo "comprende" perfectamente que se trata de un producto semiacabado, entonces el sonido metálico no. Y muy sorprendido. Lo que sucede como resultado de tal sorpresa depende "de", como dicen.


Afortunadamente, clang es bastante tolerante con los errores de código. Sin embargo, puede haber sorpresas: luz de fondo que desaparece repentinamente, curva de autocompletar, diagnósticos extraños. Necesitas estar preparado para todo esto. Además, el sonido metálico no es omnívoro. Tiene derecho a no aceptar nada en los encabezados del compilador, que aquí y ahora se usa para construir el proyecto. Las intrínsecas complicadas, las extensiones no estándar y otras, um ..., características, todo esto puede conducir a errores de análisis en los lugares más inesperados. Y, por supuesto, el rendimiento. Editar un archivo de gramática en Boost.Spirit o trabajar en un proyecto basado en llvm será un placer. Pero, sobre todo con más detalle.


Código prefabricado


Entonces, digamos que comenzó un nuevo proyecto. Su entorno generó un espacio en blanco predeterminado para main.cpp, y en él escribió:


#include <iostream> int main() { foo(10) } 

El código, desde el punto de vista de C ++, francamente, no es válido. No hay una definición de la función foo (...) en el archivo, la línea no está completa, etc. Pero ... usted acaba de comenzar. Este código tiene derecho a este tipo. ¿Cómo percibe este código un IDE con un analizador auto-escrito (en este caso, CLion)?



Y si hace clic en la bombilla, puede ver esto:



Tal IDE, sabiendo algo, um, más sobre lo que está sucediendo, ofrece la opción muy esperada: crear una función desde el contexto de uso. Gran oferta, creo. ¿Cómo se comporta el IDE basado en clang (en este caso, Qt Creator 4.7)?



¿Y qué se propone para rectificar la situación? Pero nada! ¡Solo cambio de nombre estándar!



La razón de este comportamiento es muy simple: para el sonido metálico, este texto está completo (y no puede ser otra cosa). Y él construye el AST basado en esta suposición. Y luego todo es simple: clang ve un identificador previamente indefinido. Este es texto en C ++ (no en C). No se hacen suposiciones sobre la naturaleza del identificador: no está definido, por lo que un fragmento de código no es válido. Y en AST para esta línea no aparece nada. Ella simplemente no está allí. Y lo que no está en AST es imposible de analizar. Es una pena, molesto, está bien.


El analizador incorporado en el IDE proviene de algunas otras suposiciones. Él sabe que el código no está terminado. Que el programador está ahora apresurando el pensamiento y los dedos detrás de ella no tienen tiempo. Por lo tanto, no todos los identificadores se pueden definir. Dicho código, por supuesto, es incorrecto desde el punto de vista de los altos estándares de calidad del compilador, pero el analizador sabe qué se puede hacer con dicho código y ofrece opciones. Opciones bastante razonables.


Al menos hasta la versión 3.7 (inclusive), se produjeron problemas similares en este código:


 #include <iostream> class Temp { public: int i; }; template<typename T> class Foo { public: int Bar(Temp tmp) { Tpl(tmp); } private: template<typename U> void Tpl(U val) { Foo<U> tmp(val); tmp. } int member; }; int main() { return 0; } 

Dentro de los métodos de clase de plantilla, el autocompletado basado en clang no funcionó. Por lo que pude averiguar, la razón estaba en el análisis de plantillas de dos pasos. El autocompletado en clang se activa en la primera pasada, cuando la información sobre los tipos realmente utilizados puede no ser suficiente. En clang 5.0 (a juzgar por las notas de la versión), esto se solucionó.


De una forma u otra, las situaciones en las que el compilador no puede construir el AST correcto (o sacar las conclusiones correctas del contexto) en el código editado pueden serlo. Y en este caso, el IDE simplemente no "verá" las secciones de texto correspondientes y no podrá ayudar al programador de ninguna manera. Lo cual, por supuesto, no es genial. La capacidad de trabajar eficazmente con código incorrecto es lo que necesita el analizador en el IDE y lo que el compilador normal no necesita en absoluto. Por lo tanto, el analizador en el IDE puede usar muchas heurísticas, que para el compilador pueden ser no solo inútiles, sino también dañinas. Y para implementar dos modos de funcionamiento en él, bueno, aún necesita convencer a los desarrolladores.


"¡Este papel es abusivo!"


El IDE del programador suele ser uno (bueno, dos), pero hay muchos proyectos y cadenas de herramientas. Y, por supuesto, no quiero hacer ningún gesto adicional para cambiar de cadena de herramientas a cadena de herramientas, de proyecto a proyecto. Uno o dos clics, y la configuración de compilación cambia de Debug a Release, y el compilador de MSVC a MinGW. Pero el analizador de código en el IDE sigue siendo el mismo. Y debe, junto con el sistema de construcción, cambiar de una configuración a otra, de una cadena de herramientas a otra. Una cadena de herramientas puede ser exótica o cruzada. Y la tarea del analizador aquí es continuar analizando correctamente el código. Si es posible con un mínimo de errores.


el sonido metálico es lo suficientemente omnívoro. Se puede obligar a aceptar extensiones del compilador de Microsoft, el compilador gcc. Se pueden pasar opciones en el formato de estos compiladores, y el sonido metálico incluso las entenderá. Pero todo esto no garantiza que el sonido metálico acepte cualquier partida de los menudillos recogidos del tanque de gcc. Cualquier __builtin_intrinsic_xxx puede convertirse en un obstáculo para él. O el lenguaje construye que la versión actual de clang en el IDE simplemente no es compatible. Lo más probable es que esto no afecte la calidad de la construcción AST para el archivo editado actualmente. Pero construir una base de caracteres global o guardar encabezados precompilados puede romperse. Y esto puede ser un problema grave. Un problema similar podría ser un código similar, no en los encabezados de las cadenas de herramientas o de terceros, sino en los encabezados o códigos fuente del proyecto. Por cierto, todo esto es una razón suficientemente significativa para decirle explícitamente al sistema de compilación (e IDE) sobre qué archivos de encabezado para su proyecto son "extraños". Puede hacerte la vida más fácil.


Nuevamente, el IDE se diseñó originalmente para usarse con diferentes compiladores, configuraciones, cadenas de herramientas y más. Diseñado para tener que lidiar con el código, algunos de cuyos elementos no son compatibles. El ciclo de lanzamiento del IDE (no todos :)) es más corto que el de los compiladores, por lo tanto, existe el potencial de obtener más rápidamente nuevas funciones y responder a los problemas encontrados. En el mundo de los compiladores, todo es un poco diferente: el ciclo de lanzamiento es de al menos un año, los problemas de compatibilidad entre compiladores se resuelven mediante compilación condicional y se pasan a los hombros del desarrollador. El compilador no tiene que ser universal y omnívoro: su complejidad ya es alta. Clang no es una excepción.


La lucha por la velocidad


Esa parte del tiempo que pasó en el IDE, cuando el programador no está sentado en el depurador, edita el texto. Y su deseo natural aquí es hacerlo cómodo (de lo contrario, ¿por qué un IDE? ¡Puedo pasarlo con un bloc de notas!) La comodidad, en particular, implica la alta velocidad de reacción del editor a los cambios de texto y presionar teclas de acceso rápido. Como Anastasia señaló correctamente en su informe, si cinco segundos después de presionar Ctrl + Espacio el entorno no respondía con la aparición de un menú o una lista del autocompletado, esto es terrible (en serio, pruébelo usted mismo). En números, esto significa que el analizador incorporado en el IDE tiene aproximadamente un segundo para evaluar los cambios en el archivo y reconstruir el AST, y otro uno y medio o dos para ofrecer al desarrollador una opción sensible al contexto. Segundo. Bueno, tal vez dos. Además, el comportamiento esperado es que si el desarrollador cambió el apodo .h y luego cambió a .cpp-shnik, los cambios realizados serán "visibles". Los archivos, aquí están, se abrieron en las ventanas vecinas. Y ahora un simple cálculo. Si clang, lanzado desde la línea de comando, puede hacer frente al código fuente en unos diez o veinte segundos, entonces ¿dónde está la razón para creer que cuando se inicia desde el IDE hará frente al código fuente mucho más rápido y encajará en ese segundo o dos? Es decir, ¿funcionará un orden de magnitud más rápido? En general, esto podría estar terminado, pero no lo haré.


Unos diez o veinte segundos para la fuente, por supuesto, exagero. Aunque, si se incluye alguna API pesada o, por ejemplo, boost.spirit con Hana lista, y luego todo esto se usa activamente en el texto, 10-20 segundos siguen siendo buenos valores. Pero incluso si el AST está listo segundos después de tres o cuatro después del lanzamiento del analizador incorporado, ya es mucho tiempo. Siempre que dichos lanzamientos sean tan regulares (para mantener el modelo de código y el índice en un estado coherente, resaltado, rápido, etc.), así como a pedido: la finalización del código también es el lanzamiento del compilador. ¿Es posible de alguna manera reducir este tiempo? Desafortunadamente, en el caso de usar clang como analizador sintáctico, no hay muchas posibilidades. Motivo: esta es una herramienta de terceros en la que ( idealmente ) no se pueden realizar cambios. Es decir, profundizar en el código clang con perftool, optimizar y simplificar algunas ramas: estas características no están disponibles y tiene que ver con lo que proporciona la API externa (en el caso de usar libclang, también es bastante limitado).


La primera, obvia y, de hecho, la única solución es usar encabezados precompilados generados dinámicamente. Con una implementación adecuada, la solución es asesina. Aumenta la velocidad de compilación al menos algunas veces. Su esencia es simple: el entorno recopila todos los encabezados de terceros (o encabezados fuera de la raíz del proyecto) en un solo archivo .h, crea pch a partir de este archivo y luego incluye implícitamente este pch en cada fuente. Por supuesto, aparece un efecto secundario obvio: en el código fuente ( en la etapa de edición ), se pueden ver símbolos que no están incluidos en él. Pero esto es un cargo por la velocidad. Tengo que elegir Y todo estaría bien, si no fuera por un pequeño problema: clang sigue siendo un compilador. Y, al ser un compilador, no le gustan los errores en el código. Y si de repente (¡de repente! - vea la sección anterior) hay errores en los encabezados, entonces el archivo .pch no se crea. Al menos fue hasta la versión 3.7. ¿Ha cambiado algo a este respecto desde entonces? No sé, hay una sospecha de que no. Por desgracia, ya no hay ninguna oportunidad de verificar.


Las opciones alternativas, por desgracia, no están disponibles por la misma razón: clang es un compilador y una cosa "en sí misma". Intervenir activamente en el proceso de generación de AST, de alguna manera hacer que combine AST de diferentes piezas, mantener bases de símbolos externas y te te y te - por desgracia, todas estas características no están disponibles. Solo API externa, solo hardcore y configuraciones disponibles a través de opciones de compilación. Y luego el análisis de la AST resultante. Si se sienta en la versión C ++ de la API, entonces hay un poco más de oportunidades disponibles. Por ejemplo, puede jugar con FrontendActions personalizadas, hacer ajustes más finos para las opciones de compilación, etc. Pero en este caso, el punto principal no cambiará: el texto editado (o indexado) se compilará independientemente de los demás y completamente. Eso es todo. El punto


Tal vez (¡tal vez!) Algún día habrá una bifurcación del sonido metálico aguas arriba especialmente diseñado para su uso como parte del IDE. Posiblemente Pero por ahora, todo está como está. Digamos que la integración del equipo de Qt Creator (a la etapa "final") con libclang tomó siete años. Probé QtC 4.7 con un motor basado en libclang; lo admito, personalmente me gusta la versión anterior (en la autoescrita) más simplemente porque funciona mejor en mis casos: muestra y resalta, y todo lo demás. No me comprometeré a estimar cuántas horas humanas pasaron en esta integración, pero me aventuro a sugerir que durante este tiempo sería posible terminar mi propio analizador. Por lo que puedo decir (por indicaciones indirectas), el equipo que trabaja en CLion mira con cautela hacia la integración con libclang / clang ++. Pero estos son supuestos puramente personales. La integración en el nivel del Protocolo de servidor de idiomas es una opción interesante, pero específicamente para el caso de C ++, tiendo a considerar esto más como un paliativo por las razones mencionadas anteriormente. Simplemente transfiere problemas de un nivel de abstracción a otro. Pero tal vez me confunden con el LSP: el futuro. A ver Pero de todos modos, la vida de los desarrolladores de IDEs modernos para C ++ está llena de aventuras, con el sonido metálico como backend o sin él.

Source: https://habr.com/ru/post/es419009/


All Articles