C ++: Metaprogramación con un compilador API en lugar de con características de C ++

10

Esto comenzó como una pregunta SO, pero me di cuenta de que es bastante poco convencional y, según la descripción real de los sitios web, podría ser más adecuado para los programadores, ya que la pregunta tiene mucho peso conceptual.

He estado aprendiendo LibTooling clang y es una herramienta muy poderosa capaz de exponer todo el "meollo de la cuestión" del código de una manera amigable, es decir, de una manera semántica , y no adivinando tampoco. Si clang puede compilar su código, entonces clang está seguro de la semántica de cada personaje dentro de ese código.

Ahora permíteme dar un paso atrás por un momento.

Hay muchos problemas prácticos que surgen cuando uno participa en la metaprogramación de plantillas C ++ (y especialmente cuando se aventura más allá de las plantillas en el territorio de las macros inteligentes, aunque aterradoras). Para ser honesto, para muchos programadores, incluido yo mismo, muchos de los usos comunes de las plantillas también son algo terroríficos.

Supongo que un buen ejemplo sería cadenas de tiempo de compilación . Esta es una pregunta que tiene más de un año, pero está claro que C ++ en este momento no facilita esto a los simples mortales. Si bien mirar estas opciones no es suficiente para provocarme náuseas, sin embargo, no me deja seguro de poder producir un código de máquina mágico y de máxima eficiencia para adaptarse a cualquier aplicación elegante que tenga para mi software.

Quiero decir, seamos sinceros, amigos, las cadenas son bastante simples y básicas. Algunos de nosotros solo queremos una manera conveniente de emitir código de máquina que tenga ciertas cadenas "cocidas" significativamente más de lo que obtenemos al codificarlo de manera directa. En nuestro código C ++.

Ingrese clang y LibTooling, que expone el árbol de sintaxis abstracta (AST) del código fuente y permite que una aplicación C ++ personalizada simple manipule de manera correcta y confiable el código fuente sin procesar (usando Rewriter) junto con un rico modelo semántico orientado a objetos de todo en el AST. Maneja muchas cosas. Conoce las macro expansiones y te permite seguir esas cadenas. Sí, estoy hablando de la transformación o traducción del código fuente a fuente.

Mi tesis fundamental aquí es que el sonido metálico ahora nos permite crear ejecutables que pueden funcionar como las etapas de preprocesador personalizadas ideales para nuestro software C ++, y podemos implementar estas etapas de metaprogramación con C ++. Simplemente estamos limitados por el hecho de que esta etapa debe tomar la entrada que es un código C ++ válido y producir como salida un código C ++ más válido. Además de cualquier otra restricción que aplique su sistema de compilación.

La entrada debe estar al menos muy cerca de un código C ++ válido porque, después de todo, clang es el front-end del compilador y solo estamos hurgando y siendo creativos con su API. No sé si hay alguna disposición para poder definir una nueva sintaxis para usar, pero claramente tenemos que desarrollar las formas de analizarla correctamente y agregarla al proyecto clang para poder hacer esto. Esperar más es tener algo en el proyecto clang que está fuera de alcance.

No es un problema. Me imagino que algunas funciones macro no-op pueden manejar esta tarea.

Otra forma de ver lo que estoy describiendo es implementar construcciones de metaprogramación usando C ++ en tiempo de ejecución manipulando el AST de nuestro código fuente (gracias a clang y su API) en lugar de implementarlas usando las herramientas más limitadas disponibles en el lenguaje mismo. Esto también tiene claros beneficios en el rendimiento de la compilación (los encabezados con plantillas pesadas ralentizan la compilación proporcionalmente a la frecuencia con la que los usas. Muchas cosas compiladas son cuidadosamente emparejadas y desechadas por el enlazador).

Sin embargo, esto tiene el costo de introducir uno o dos pasos adicionales en el proceso de compilación y también el requisito de escribir un software (ciertamente) algo más detallado (pero al menos es un tiempo de ejecución C ++ sencillo) como parte de nuestra herramienta .

Esa no es la imagen completa. Estoy bastante seguro de que existe un espacio mucho más amplio de funcionalidad que se puede obtener generando código que es extremadamente difícil o imposible con las características principales del lenguaje. En C ++ puede escribir una plantilla o una macro o una combinación loca de ambos, pero en una herramienta de sonido puede modificar clases y funciones de CUALQUIER manera que pueda lograr con C ++, en tiempo de ejecución , mientras tiene acceso completo al contenido semántico, Además de plantilla y macros y todo lo demás.

Entonces, me pregunto por qué todos ya no están haciendo esto. ¿Es que esta funcionalidad de clang es tan nueva y nadie está familiarizado con la enorme jerarquía de clases de AST de clang? Eso no puede ser.

Quizás solo estoy subestimando la dificultad de esto un poco, pero hacer "manipulación de cadenas en tiempo de compilación" con una herramienta de sonido metálico es casi criminalmente simple. Es detallado, pero es increíblemente sencillo. Todo lo que se necesita es un montón de funciones macro no operativas que se correlacionen con std::stringoperaciones reales reales . El complemento clang implementa esto mediante la obtención de todas las llamadas macro no-op relevantes, y realiza las operaciones con cadenas. Esta herramienta luego se inserta como parte del proceso de construcción. Durante la compilación, estas llamadas a funciones de macro no operativas se evalúan automáticamente en sus resultados y luego se vuelven a insertar como cadenas de tiempo de compilación simples en el programa. El programa se puede compilar como de costumbre. De hecho, este programa resultante también es mucho más portátil, ya que no requiere un nuevo compilador elegante que admita C ++ 11.

Steven Lu
fuente
Esta es una pregunta inusualmente larga. ¿Podría quizás condensarlo en sus puntos más relevantes?
amon
Publico muchas preguntas largas. Pero especialmente con este, creo que todas las partes de la pregunta son importantes. Tal vez omita los primeros 6 párrafos? Jaja.
Steven Lu
3
Se parece mucho a las macros sintácticas iniciadas en Lisp y recientemente recogidas por Haxe, Nemerle, Scala y lenguajes similares. Hay bastante lectura sobre por qué las macros de Lisp se consideran dañinas. Aunque todavía no he escuchado un argumento convincente, es posible que encuentre razones por las cuales las personas han sido reacias a agregarlas a todos los idiomas (además del hecho de que no es necesariamente sencillo).
back2dos
Sí, es meta-ifying C ++. Lo que puede significar un código mejor y más rápido. En cuanto a esos idiomas. Bueno, por donde empezaré. ¿Qué es un videojuego multimillonario implementado en cualquiera de esos idiomas? ¿Qué es un navegador web moderno implementado en cualquiera de esos idiomas? Núcleo del sistema operativo? Muy bien, en realidad parece que Haxe tiene cierta cantidad de tracción, pero entiendes la idea.
Steven Lu
1
@nwp, Bueno, no puedo evitar señalar que parece que te has perdido todo el punto de la publicación. Las cadenas en tiempo de compilación son simplemente el ejemplo concreto más ingenioso y mínimo de las capacidades disponibles para nosotros ahora.
Steven Lu

Respuestas:

7

Sí, Virginia, hay un Papá Noel.

La noción de usar programas para modificar programas ha existido desde hace mucho tiempo. La idea original surgió de John von Neumann en forma de computadoras con programas almacenados. Pero el código de máquina que modifica el código de máquina de manera arbitraria es bastante inconveniente.

La gente generalmente quiere modificar el código fuente. Esto se realiza principalmente en forma de sistemas de transformación de programas (PTS) .

PTS generalmente ofrece, al menos para un lenguaje de programación, la capacidad de analizar AST, manipular ese AST y regenerar texto fuente válido. Si de hecho cava, para la mayoría de los lenguajes convencionales, alguien ha creado una herramienta de este tipo (Clang es un ejemplo para C ++, el compilador de Java ofrece esta capacidad como API, Microsoft ofrece Rosyln, JDT de Eclipse, ...) con un procedimiento API que en realidad es bastante útil. Para la comunidad en general, casi todas las comunidades específicas del idioma pueden apuntar a algo como esto, implementado con varios niveles de madurez (generalmente modesto, muchos "analizadores que producen AST"). Feliz metaprogramación.

[Hay una comunidad orientada a la reflexión que intenta hacer metaprogramación desde el interior del lenguaje de programación, pero solo logra la modificación del comportamiento de "tiempo de ejecución", y solo en la medida en que los compiladores del lenguaje ponen a disposición cierta información por reflexión. Con la excepción de LISP, siempre hay detalles sobre el programa que no están disponibles por reflexión ("Luke, necesitas la fuente") que siempre limitan lo que la reflexión puede hacer.]

Los PTS más interesantes hacen esto para los idiomas arbitrarios (le da a la herramienta una descripción del idioma como parámetro de configuración, incluido como mínimo el BNF). Tal PTS también le permite realizar una transformación "fuente a fuente", por ejemplo, especificar patrones directamente utilizando la sintaxis de superficie del lenguaje de destino; utilizando dichos patrones, puede codificar fragmentos de interés y / o buscar y reemplazar fragmentos de código. Esto es mucho más conveniente que la API de programación, porque no tiene que conocer todos los detalles microscópicos sobre los AST para hacer la mayor parte de su trabajo. Piense en esto como meta-metaprogramación: -}

Una desventaja: a menos que el PTS ofrezca varios tipos de análisis estáticos útiles (tablas de símbolos, control y análisis de flujo de datos), es difícil escribir transformaciones realmente interesantes de esta manera, porque necesita verificar los tipos y verificar los flujos de información para la mayoría de las tareas prácticas. Desafortunadamente, esta capacidad es de hecho rara en el PTS general. (Siempre no está disponible con el siempre propuesto "Si acabo de tener un analizador ..." Vea mi biografía para una discusión más larga de "La vida después de analizar").

Hay un teorema que dice que si puede hacer una reescritura de cadenas [por lo tanto, una reescritura de árbol] puede hacer una transformación arbitraria; y, por lo tanto, una serie de PTS se apoyan en esto para afirmar que puede metaprogramar cualquier cosa con solo las reescrituras de árbol que ofrecen. Si bien el teorema es satisfactorio en el sentido de que ahora está seguro de que puede hacer cualquier cosa, no es satisfactorio de la misma manera que la capacidad de una máquina de Turing para hacer cualquier cosa no hace que la programación de una máquina de Turing sea el método de elección. (Lo mismo es cierto para los sistemas con solo API de procedimiento, si le permiten realizar cambios arbitrarios en el AST [y, de hecho, creo que esto no es cierto para Clang]).

Lo que quiere es lo mejor de ambos mundos, un sistema que le ofrece la generalidad del tipo de PTS parametrizado por idioma (incluso manejando múltiples idiomas), con análisis estáticos adicionales, la capacidad de mezclar transformaciones de fuente a fuente con procedimientos APIs. Solo sé de dos que hacen esto:

  • Lenguaje de metaprogramación Rascal (MPL)
  • nuestro kit de herramientas de reingeniería de software DMS

A menos que desee escribir las descripciones del lenguaje y los analizadores estáticos usted mismo (para C ++ esto es una gran cantidad de trabajo, por eso Clang fue construido como un compilador y como una base de metaprogramación de procedimiento general), querrá un PTS con descripciones de lenguaje maduro ya disponible. De lo contrario, pasará todo su tiempo configurando el PTS, y ninguno hará el trabajo que realmente quería hacer. [Si elige un lenguaje aleatorio, no convencional, este paso es muy difícil de evitar].

Rascal intenta hacer esto cooptando "OPP" (Otros analizadores de personas) pero eso no ayuda con la parte de análisis estático. Creo que tienen Java bastante bien en la mano, pero estoy muy seguro de que no hacen C o C ++. Pero, es una herramienta de investigación académica; Es difícil culparlos.

Destaco que nuestra herramienta DMS [comercial] tiene Java, C, C ++ front-end disponibles. Para C ++, cubre casi todo en C ++ 14 para GCC e incluso las variaciones de Microsoft (y estamos puliendo ahora), expansión macro y administración condicional, y control de nivel de método y análisis de flujo de datos. Y sí, puede especificar los cambios gramaticales de manera práctica; Creamos un sistema VectorC ++ personalizado para un cliente que extendió radicalmente C ++ para usar lo que equivale a las operaciones de matriz paralela de datos F90 / APL. DMS se ha utilizado para llevar a cabo otras tareas de metaprogramación masiva en grandes sistemas C ++ (por ejemplo, remodelación arquitectónica de aplicaciones). (Soy el arquitecto detrás de DMS).

Feliz meta-metaprogramación.

Ira Baxter
fuente
Genial, creo que Clang y DMS, si bien tienen algunas capacidades superpuestas, son piezas de software que no están realmente en la misma categoría. Quiero decir, uno es probablemente ridículamente caro y probablemente nunca podría justificar los recursos que se necesitarían para obtener acceso a él, y el otro es de código abierto libre y sin restricciones. Esta es una gran diferencia ... Parte de lo que me entusiasma de estas emocionantes capacidades de metaprogramación es el hecho de que está permitido no solo usarlo libremente sino también distribuir libremente herramientas binarias basadas en el sonido metálico.
Steven Lu
Todo lo que se vende comercialmente es "ridículamente caro" en comparación con gratis. El costo bruto no es el problema; lo que importa es que, para algunas personas, la recuperación de la inversión para adquirir el producto comercial es mayor que la recuperación de la inversión para el artefacto gratuito, de lo contrario no habría software comercial. Obviamente, esto depende de sus necesidades específicas. Clang es un punto interesante en el espacio de herramientas, y ciertamente tendrá puntos de aplicación útiles. Me gustaría pensar (ya que soy el arquitecto de DMS) que DMS tiene capacidades más amplias. Es improbable que Clang sea compatible con otros lenguajes que no sean C ++, por ejemplo.
Ira Baxter
Ciertamente. No hay duda de que DMS es increíblemente poderoso, casi hasta el punto de la magia (al estilo de Arthur C. Clarke), y aunque el sonido metálico es excelente, en realidad es solo una interfaz de C ++ bien escrita, de la cual hay muchas. Una gran cantidad de pequeños pasos hacia adelante, es decir, todavía no sería realmente justo compararlo con DMS. Por desgracia, incluso con herramientas tan poderosas a nuestra disposición, el software de trabajo no se escribe solo. Todavía debe existir a través de una traducción cuidadosa utilizando las herramientas, o (casi siempre la opción superior) escrita de nuevo.
Steven Lu
No puede permitirse el lujo de crear herramientas como Clang o DMS desde cero. Tampoco puede darse el lujo de lanzar esa solicitud que escribió con un equipo de 10 durante 5 años. Necesitaremos tales herramientas cada vez con más frecuencia, a medida que los tamaños de software y la vida útil continúen creciendo.
Ira Baxter
@StevenLu: Bueno, DMS te agradece el cumplido, pero no tiene nada de mágico. DMS tiene el beneficio de casi 2 décadas lineales de ingeniería y una plataforma arquitectónica limpia (aw, shucks, YMMV) que se ha mantenido bastante bien. Del mismo modo, Clang tiene mucha buena ingeniería. Estoy de acuerdo, no fueron diseñados para resolver exactamente el mismo problema ... El alcance de DMS está explícitamente destinado a ser más grande cuando se trata de la manipulación simbólica del programa, y ​​mucho más pequeño cuando se trata de ser un compilador de producción.
Ira Baxter
4

La metaprogramación en C ++ con la API de compiladores (en lugar de usar plantillas) es realmente interesante y prácticamente posible. Dado que la metaprogramación no está (todavía) estandarizada, estará vinculado a un compilador específico, que no es el caso con las plantillas.

Entonces, me pregunto por qué todos ya no están haciendo esto. ¿Es que esta funcionalidad de clang es tan nueva y nadie está familiarizado con la enorme jerarquía de clases de AST de clang? Eso no puede ser.

Mucha gente hace esto, pero en otros idiomas. Mi opinión es que la mayoría de los desarrolladores de C ++ (o Java, o C) no ven la necesidad (tal vez con razón), o no están acostumbrados a los enfoques de metaprogramación; También creo que están contentos con las características de refactorización / generación de código de su IDE, y que cualquier cosa más elegante podría verse demasiado compleja / difícil de mantener / difícil de depurar. Sin las herramientas adecuadas, podría ser cierto. También debe tener en cuenta la inercia y otros problemas no técnicos, como contratar y / o capacitar personas.

Por cierto, dado que estamos mencionando Common Lisp y su sistema macro (ver la respuesta de Basile), debo decir que ayer mismo, Clasp fue liberado (no estoy afiliado):

Clasp pretende ser una implementación de Common Lisp conforme que compila a LLVM IR. Además, expone las bibliotecas de Clang (AST, Matcher) al desarrollador.

  • Primero, eso significa que podría escribir en CL y no usar más C ++, excepto cuando use sus bibliotecas (y si necesita macros, use macros CL).

  • En segundo lugar, puede escribir herramientas en CL para su código C ++ existente (análisis, refactorización, ...).

volcado de memoria
fuente
3

Varios compiladores de C ++ tienen una API más o menos documentada y estable, en particular la mayoría de los compiladores de software libre.

Clang / LLVM es principalmente un gran conjunto de bibliotecas, y podría usarlas.

GCC reciente está aceptando complementos . En particular, puede extenderlo usando MELT (que en sí mismo es un meta-complemento, que le proporciona un lenguaje específico de dominio de nivel superior para extender GCC).

Tenga en cuenta que la sintaxis de C ++ no es fácilmente extensible en GCC (y probablemente tampoco en Clang), pero puede agregar sus propios pragmas, componentes incorporados, atributos y pases de compilación para hacer lo que desee (quizás también proporcione algunas macros de preprocesador que invoquen estas cosas) para dar una sintaxis fácil de usar).

Puede que le interesen los compiladores y los lenguajes de etapas múltiples; consulte, por ejemplo, el documento Implementación de lenguajes de etapas múltiples utilizando AST, Gensym y el documento de reflexión de C.Calcagno et al. y trabajar alrededor de MetaOcaml . Sin duda, debe mirar dentro de las macro instalaciones de Common Lisp . Y usted podría estar interesado por las bibliotecas JIT como libjit , un rayo de GNU , incluso LLVM , o simplemente -al tiempo de ejecución - generar algo de código C ++, tenedor una compilación de ella en una biblioteca de objeto dinámico compartido, entonces dlopen (3) que compartían objeto. El blog de J.Pitrat también está relacionado con dichos enfoques reflexivos. Y también RefPerSys .

Basile Starynkevitch
fuente
Interesante. Es muy bueno ver que GCC continúa evolucionando aquí. Esta no es una respuesta que aborde todo lo que he preguntado, pero de todos modos me gusta.
Steven Lu
Re: sus nuevas ediciones ... Ese es un buen punto sobre la reescritura de código en sí. En realidad, esto también comienza a brindar dicha capacidad de metaprograma a C ++, mucho más accesible que antes, lo que también es bastante interesante.
Steven Lu