sistemica - tecnicas estructurales en terapia familiar
Apoyo de reflexión en C (10)
¿Algún truco a su alrededor? ¿Algun consejo?
El compilador probablemente genere opcionalmente un ''archivo de símbolos de depuración'', que un depurador puede usar para ayudar a depurar el código. El enlazador también puede generar un ''archivo de mapa''.
Un truco / consejo podría ser generar y luego leer estos archivos.
Sé que no es compatible, pero me pregunto si hay algún truco a su alrededor. ¿Algun consejo?
- Implementar la reflexión para C sería mucho más simple ... porque C es un lenguaje simple.
- Hay algunas opciones básicas para analizar el programa, como detectar si existe una función al llamar a dlopen / dlsym - depende de sus necesidades.
- Existen herramientas para crear código que pueden modificarse / extenderse utilizando tcc .
- Puede utilizar la herramienta anterior para crear sus propios analizadores de código.
Basado en las respuestas a ¿Cómo puedo agregar reflexión a una aplicación de C ++? (Desbordamiento de pila) y el hecho de que C ++ se considera un "superconjunto" de C, diría que estás fuera de suerte.
También hay una buena respuesta larga sobre por qué C ++ no tiene reflejo (Desbordamiento de pila) .
Conozco las siguientes opciones, pero todas tienen un costo y muchas limitaciones:
- Utilice
libdl
(#include <dfcln.h>
) - Llame a una herramienta como
objdump
onm
- Analice usted mismo los archivos de objetos (utilizando la biblioteca correspondiente)
- Involucre a un analizador y genere la información necesaria en tiempo de compilación.
- "Abuso" del enlazador para generar matrices de símbolos.
Usaré un poco de marcos de prueba de unidad como ejemplos más abajo, porque el descubrimiento automático de pruebas para marcos de prueba de unidad es un ejemplo típico donde la reflexión es muy útil, y es algo que la mayoría de los marcos de prueba de unidad para C no alcanzan.
Utilizando libdl
( #include <dfcln.h>
) (POSIX)
Si está en un entorno POSIX, se puede hacer un poco de reflexión utilizando libdl
. Los complementos se desarrollan de esa manera.
Utilizar
#include <dfcln.h>
en su código fuente y enlace con -ldl
.
Luego tiene acceso a las funciones dlopen()
, dlerror()
, dlsym()
y dlclose()
con las que puede cargar y acceder / ejecutar objetos compartidos en tiempo de ejecución. Sin embargo, no le da acceso fácil a la tabla de símbolos.
Otra desventaja de este enfoque es que básicamente restringe la reflexión a los objetos cargados como biblioteca dinámica (objeto compartido cargado en tiempo de ejecución a través de dlopen()
).
Ejecutando nm
u objdump
Puede ejecutar nm
u objdump
para mostrar la tabla de símbolos y analizar la salida. Para mí, nm -P --defined-only -g xyz.o
da buenos resultados y el análisis de la salida es trivial. Le interesaría solo la primera palabra de cada línea, que es el nombre del símbolo, y quizás la segunda, que es el tipo de sección.
Si no conoce el nombre del objeto de alguna manera estática, es decir, el objeto es en realidad un objeto compartido, al menos en Linux, es posible que desee omitir los nombres de los símbolos que comienzan con ''_''.
objdump
herramientas objdump
, nm
o similares también suelen estar disponibles fuera de los entornos POSIX.
Analizar los archivos de objeto usted mismo
Usted podría analizar los archivos de objetos a ti mismo. Probablemente no quiera implementar eso desde cero, pero use una biblioteca existente para eso. Así es como se implementan nm
, objdump
e incluso libdl
. Puedes ver el código fuente de nm
, objdump
y libdl
y las bibliotecas que usan para descubrir cómo hacen lo que hacen.
Involucrando un analizador
Podría escribir un analizador y un generador de código que genere la información reflexiva necesaria en el momento de la compilación y la almacene en el archivo objeto. Entonces tienes mucha libertad e incluso podrías implementar formas primitivas de anotaciones. Eso es lo que hacen algunos marcos de prueba de unidad como AceUnit .
Encontré que escribir un analizador que cubra la sintaxis C directa es bastante trivial. Escribir un analizador que realmente entienda C y pueda tratar todos los casos NO es trivial. Entonces, esto tiene limitaciones que dependen de qué tan exótica es la sintaxis de C sobre la que desea reflexionar.
"Abusando" del enlazador para generar matrices de símbolos.
Puede poner referencias a los símbolos que desea reflejar en una sección especial y usar una configuración de enlace para emitir los límites de la sección para que pueda acceder a ellos en C.
He descrito aquí la inyección de dependencia N en C: ¿es mejor que las matrices definidas por el enlazador? como funciona esto
Pero cuidado, esto depende de muchas cosas y no es muy portátil. Solo he probado esto con GCC
/ ld
, y sé que no funciona con todos los compiladores / enlazadores. Además, es casi seguro que la eliminación del código muerto no detectará cómo llamas a esto, por lo que si utilizas la eliminación del código muerto, tendrás que agregar todos los símbolos reflejados como puntos de entrada.
Escollos
Para algunos de los mecanismos, la eliminación del código muerto puede ser un problema, en particular cuando se "abusa" del vinculador para generar una matriz de símbolos. Se puede solucionar diciendo los símbolos reflejados como puntos de entrada al vinculador, y dependiendo de la cantidad de símbolos, esto podría no ser agradable ni conveniente.
Conclusión
La combinación de nm
y libdl
puede dar resultados bastante buenos. La combinación puede ser casi tan poderosa como el nivel de Reflexión usado por JUnit 3.x en Java. El nivel de reflexión dado es suficiente para implementar un marco de prueba de unidad estilo JUnit 3.x para C, incluido el descubrimiento de casos de prueba mediante la convención de denominación.
Involucrar a un analizador es más trabajo y está limitado a los objetos que usted mismo compila, pero le brinda más poder y libertad. El nivel de reflexión dado puede ser suficiente para implementar un marco de prueba de unidad estilo JUnit 4.x para C, incluido el descubrimiento de casos de prueba por anotaciones. AceUnit es un marco de prueba de unidad para C que hace exactamente esto.
La combinación del análisis y el enlazador para generar matrices de símbolos puede dar muy buenos resultados: si su entorno está bajo su control, puede asegurarse de que trabajar con el enlazador de esa manera funcione para usted.
Y, por supuesto, puede combinar todos los enfoques para unir las brocas y las piezas hasta que se ajusten a sus necesidades.
Consejos y trucos siempre existen. Echa un vistazo a la biblioteca Metaresc https://github.com/alexanderchuranov/Metaresc
Proporciona una interfaz para la declaración de tipos que también generará metadatos para el tipo. Basándose en metadatos, puede serializar / deserializar fácilmente objetos de cualquier complejidad. Fuera de la caja puede serializar / deserializar XML, JSON, XDR, notación tipo Lisp, notación C-init.
Aquí hay un ejemplo simple:
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include "metaresc.h"
TYPEDEF_STRUCT (point_t,
double x,
double y
);
int main (int argc, char * argv[])
{
point_t point = {
.x = M_PI,
.y = M_E,
};
char * str = MR_SAVE_XML (point_t, &point);
if (str)
{
printf ("%s/n", str);
free (str);
}
return (EXIT_SUCCESS);
}
Este programa producirá
$ ./point
<?xml version="1.0"?>
<point>
<x>3.1415926535897931</x>
<y>2.7182818284590451</y>
</point>
La biblioteca funciona bien para los últimos gcc y clang.
La reflexión en general es un medio para que un programa analice la estructura de algún código. Este análisis se utiliza para cambiar el comportamiento efectivo del código.
La reflexión como análisis es generalmente muy débil; por lo general, solo puede proporcionar acceso a los nombres de funciones y campos. Esta debilidad proviene de los implementadores de lenguaje que, básicamente, no desean que el código fuente completo esté disponible en tiempo de ejecución, junto con las rutinas de análisis apropiadas para extraer lo que uno quiere del código fuente.
Otro enfoque es abordar el análisis de programas de frente, usando una herramienta de análisis de programas sólida, por ejemplo, una que pueda analizar el texto de origen exactamente de la manera en que lo hace el compilador. (A menudo, la gente propone abusar del compilador para hacer esto, pero eso no suele funcionar; la maquinaria del compilador quiere ser un compilador y es muy difícil doblarlo para otros propósitos).
Lo que se necesita es una herramienta que:
- Analiza el texto fuente del idioma
- Construye árboles de sintaxis abstractos que representan cada detalle del programa. (Es útil si los AST conservan los comentarios y otros detalles del diseño del código fuente, como los números de columna, los valores de radix literal, etc.)
- Crea tablas de símbolos que muestran el alcance y el significado de cada identificador
- Puede extraer flujos de control de funciones.
- Puede extraer datos desde el código.
- Puede construir un gráfico de llamadas para el sistema.
- Puede determinar a qué apunta cada puntero.
- Permite la construcción de analizadores personalizados utilizando los hechos anteriores.
- Puede transformar el código de acuerdo con dichos análisis personalizados (generalmente revisando los AST que representan el código analizado)
- Puede regenerar el texto de origen (incluido el diseño y los comentarios) a partir de los AST revisados.
Usando dicha maquinaria, uno implementa el análisis en el nivel de detalle que sea necesario y luego transforma el código para lograr el efecto que lograría la reflexión en tiempo de ejecución. Hay varios beneficios importantes:
- El nivel de detalle o la cantidad de análisis es una cuestión de ambición (por ejemplo, no está limitado por lo que solo puede hacer la reflexión en tiempo de ejecución)
- No hay ninguna sobrecarga de tiempo de ejecución para lograr el cambio reflejado en el comportamiento
- La maquinaria involucrada puede ser general y aplicarse en muchos idiomas, en lugar de limitarse a lo que proporciona una implementación de lenguaje específica.
- Esto es compatible con la idea de C / C ++ de que no paga por lo que no usa. Si no necesitas reflexión, no necesitas esta maquinaria. Y su idioma no necesita tener incorporado el bagaje intelectual de la reflexión débil.
Consulte nuestro kit de herramientas de reingeniería de software DMS para ver un sistema que puede hacer todo lo anterior para C, Java y COBOL, y la mayoría para C ++.
Los analizadores y los símbolos de depuración son grandes ideas. Sin embargo, el gotcha es que C no tiene matrices realmente. Sólo punteros a cosas.
Por ejemplo, no hay forma de leer el código fuente para saber si un char * apunta a un carácter, una cadena o una matriz fija de bytes en función de algún campo de longitud "cercana". Este es un problema para los lectores humanos y mucho menos para cualquier herramienta automatizada.
¿Por qué no usar un lenguaje moderno, como Java o .Net? Puede ser más rápido que C también.
Necesitarías implementarlo desde el principio. En C recta, no hay información de tiempo de ejecución en la estructura y tipos compuestos. Los metadatos simplemente no existen en el estándar.
Simplemente haga cualquier tipo de tabla, árbol, lista enlazada o cualquier colección con llave que se sienta cómodo manejando o que encuentre efectivo. Agregue una clave, ya sea una cadena, combo / id combo o lo que sea, y apunte la dirección a una función o estructura. Una versión súper ingenua de reflexión puede ser una colección de lo siguiente:
struct reflectable{
size_t size,id,type; // describes payload
char* name;
void* payload;
}
Con un caso de interruptor grande donde se hace un controlador para cada tipo o nombre o macros para adjuntar el mismo. Alternativamente, siempre adjunte funciones que sean receptores de lo que sea que esté en su estructura reflejable, que sean las mismas que las de los manejadores, pero con más de un modelo de despacho directamente en su contenedor.
Necesitaba reflexión en un montón de struct
en un proyecto de C ++.
Creé un archivo xml con la descripción de todas esas estructuras; afortunadamente, los tipos de campos eran tipos primitivos.
Utilicé una plantilla (no una plantilla de C ++) para generar automáticamente una class
para cada struct
junto con los métodos de establecimiento / obtención.
En cada class
utilicé un mapa para asociar nombres de cadenas y miembros de clase (punteros a miembros).
No me arrepentí de usar la reflexión porque abrió nuevas formas de diseñar mi funcionalidad principal que ni siquiera podía imaginar sin la reflexión.
(Por cierto, fue un generador de informes externo para un programa que utiliza una base de datos sin procesar )
Entonces, utilicé generación de código, punteros de función y mapas para simular la reflexión.