¿Cómo diseñar un programa C ++ para permitir la importación de funciones en tiempo de ejecución?

10

hoy, me gustaría hacerle una pregunta sobre las capacidades de C ++ para realizar una arquitectura de software específica.

Por supuesto, he usado la búsqueda pero no he encontrado ninguna respuesta directamente vinculada.

Básicamente, mi objetivo es construir un programa que permita al usuario modelar y simular sistemas físicos compuestos arbitrariamente, por ejemplo, un automóvil que conduce. Supongo que tengo una biblioteca de modelos físicos (funciones dentro de las clases). Cada función puede tener algunas entradas y devolver algunas salidas dependiendo de la descripción física subyacente, por ejemplo, un modelo de motor de combustión, un modelo de resistencia aerodinámica, un modelo de rueda, etc.

Ahora, la idea es proporcionar al usuario un marco que le permita componer cualquier función de acuerdo con sus necesidades, es decir, mapear cualquier comportamiento físico. El marco debe proporcionar funcionalidades para conectar las salidas y entradas de diferentes funciones. Por lo tanto, el marco proporciona una clase de contenedor. Lo llamo COMPONENTE, que puede contener uno o varios objetos modelo (FUNCIÓN). Estos contenedores también pueden contener otros componentes (cf. patrón compuesto), así como las conexiones (CONECTOR) entre los parámetros de la función. Además, la clase de componente proporciona algunas funcionalidades numéricas generales, como el solucionador matemático, etc.

La composición de las funciones debe hacerse durante el tiempo de ejecución. En primera instancia, el usuario debe poder configurar una composición mediante la importación de un XML que define la estructura de la composición. Más tarde, uno podría pensar en agregar una GUI.

Para darle una mejor comprensión, aquí hay un ejemplo muy simplificado:

<COMPONENT name="Main">
  <COMPONENT name="A">
    <FUNCTION name="A1" path="lib/functionA1" />
  </COMPONENT>
  <COMPONENT name="B">
    <FUNCTION name="B1" path="lib/functionB1" />
    <FUNCTION name="B2" path="lib/functionB2" />
  </COMPONENT>
  <CONNECTIONS>
    <CONNECTOR source="A1" target="B1" />
    <CONNECTOR source="B1" target="B2" />
  </CONNECTIONS>        
</COMPONENT>

No es necesario profundizar en las capacidades del marco porque mi problema es mucho más general. Cuando se compila el código / programa marco, no se conocen la descripción del problema físico ni las funciones definidas por el usuario. Cuando el usuario selecciona (a través de XML o posterior a través de una GUI) una función, el marco debe leer la información de la función, es decir, debe obtener la información de los parámetros de entrada y salida, para ofrecer al usuario la opción de interconectar las funciones.

Conozco los principios de la reflexión y soy consciente de que C ++ no proporciona esta función. Sin embargo, estoy seguro de que con frecuencia se requiere el concepto de "construir objetos durante el tiempo de ejecución". ¿Cómo debo configurar mi arquitectura de software en C ++ para lograr mi objetivo? ¿Es C ++ el lenguaje correcto? ¿Qué paso por alto?

¡Gracias por adelantado!

Saludos, Oliver

Oliver
fuente
C ++ tiene punteros de función y objetos de función. ¿Todas las funciones están compiladas en el ejecutable o están en bibliotecas dinámicas (en qué plataforma)?
Caleth
1
La pregunta es demasiado amplia en el sentido de que generalmente requiere un título universitario en ingeniería eléctrica / [automatización de diseño electrónico (EDA)] ( en.wikipedia.org/wiki/Electronic_design_automation ) o ingeniería mecánica / diseño asistido por computadora (CAD) . En términos comparativos, llamar a la biblioteca dinámica C / C ++ es muy fácil, consulte Convenciones de llamadas C para x86 . Sin embargo, puede requerir manipular la pila (a través del puntero de la pila de la CPU) y los valores de registro de la CPU.
rwong
1
Las funciones de carga dinámica no son compatibles con el lenguaje C ++. Tendrás que mirar algo específico de la plataforma. Por ejemplo, un compilador de C ++ en Windows debería admitir DLL de Windows, que sí admiten una forma de reflexión.
Simon B
En C ++ es realmente difícil llamar a una función cuya firma (tipos de argumento y retorno) no se conoce en tiempo de compilación. Para hacerlo, debe saber cómo funcionan las llamadas de función en el nivel de ensamblado de la plataforma elegida.
Bart van Ingen Schenau el
2
La forma en que resolvería esto es compilar código de C ++ que cree un intérprete para cualquier lenguaje que admita un comando eval. Problema de Bang resuelto usando c ++. : P Piensa por qué eso no es lo suficientemente bueno y actualiza la pregunta. Ayuda cuando los requisitos reales son claros.
candied_orange

Respuestas:

13

En C ++ estándar puro, no puede "permitir la importación de funciones en tiempo de ejecución"; De acuerdo con el estándar, el conjunto de funciones de C ++ se conoce estáticamente en tiempo de compilación (en la práctica, tiempo de enlace) ya que se solucionó desde la unión de todas las unidades de traducción que componen su programa.

En la práctica, la mayoría de las veces (excluyendo los sistemas integrados) su programa C ++ se ejecuta por encima de algún sistema operativo . Lea Sistemas operativos: tres piezas fáciles para una buena descripción general.

Varios sistemas operativos modernos permiten la carga dinámica de complementos . POSIX especifica notablemente dlopen& dlsym. Windows tiene algo diferente LoadLibrary(y un modelo de enlace inferior; debe anotar explícitamente las funciones correspondientes, proporcionadas o utilizadas por los complementos). Por cierto, en Linux puedes prácticamente dlopenuna gran cantidad de complementos (mira mi manydl.cprograma , con la suficiente paciencia que puede generar y luego cargar casi un millón de complementos). Entonces, lo de XML podría impulsar la carga de complementos. La descripción de su multicomponente / multiconector me recuerda las señales Qt y las ranuras (que requieren un mocpreprocesador ; es posible que también necesite algo así).

La mayoría de las implementaciones de C ++ usan el cambio de nombre . Por eso, será mejor que declares como extern "C"las funciones relacionadas con los complementos (y definidos en ellos, y accedidos dlsymdesde el programa principal). Lea el mini HowTo de C ++ dlopen (al menos para Linux).

Por cierto, Qt y POCO son marcos C ++ que proporcionan un enfoque portátil y de alto nivel para los complementos. Y libffi le permite llamar a funciones cuya firma solo se conoce en tiempo de ejecución.

Otra posibilidad es incrustar algún intérprete, como Lua o Guile , en su programa (o escribir uno propio, como lo hizo Emacs). Esta es una fuerte decisión de diseño arquitectónico. Es posible que desee leer Lisp In Small Pieces y Programming Language Pragmatics para obtener más información.

Hay variantes o mezclas de esos enfoques. Podría usar alguna biblioteca de compilación JIT (como libgccjit o asmjit). Podría generar en tiempo de ejecución algún código C y C ++ en un archivo temporal, compilarlo como un complemento temporal y cargarlo dinámicamente (utilicé este enfoque en GCC MELT ).

En todos estos enfoques, la administración de memoria es una preocupación importante (es una propiedad de "programa completo", y lo que en realidad es el "sobre" de su programa está "cambiando"). Necesitarás al menos algo de cultura sobre la recolección de basura . Lea el manual de GC para la terminología. En muchos casos ( referencias circulares arbitrarias donde los punteros débiles no son predecibles), el esquema de recuento de referencias estimado por los punteros inteligentes C ++ podría no ser suficiente. Ver también esto .

Lea también sobre la actualización dinámica de software .

Tenga en cuenta que algunos lenguajes de programación, especialmente Common Lisp (y Smalltalk ), son más amigables con la idea de importar funciones en tiempo de ejecución. SBCL es una implementación de software gratuita de Common Lisp, y compila el código de la máquina en cada interacción REPL (e incluso puede recolectar basura del código de la máquina, y puede guardar un archivo de imagen central completo que luego puede reiniciarse fácilmente).

Basile Starynkevitch
fuente
3

Claramente está tratando de desarrollar su propio estilo de software tipo Simulink o LabVIEW, pero con un componente XML impuro.

En su forma más básica, está buscando una estructura de datos orientada a gráficos. Sus modelos físicos están formados por nodos (los llama componentes) y bordes (conectores en sus nombres).

No hay un mecanismo forzado por el lenguaje para hacer esto, ni siquiera con reflexión, por lo que deberá crear una API y cualquier componente que quiera jugar tendrá que implementar varias funciones y cumplir con las reglas establecidas por su API.

Cada componente deberá implementar un conjunto de funciones para hacer cosas como:

  • Obtenga el nombre del componente u otros detalles al respecto
  • Obtenga el número de cuántas entradas o salidas expone el componente
  • Interrogar un componente sobre una entrada particular nuestra salida
  • Conecte entradas y salidas juntas
  • y otros

Y eso es solo para configurar su gráfico. Necesitará funciones adicionales definidas para organizar cómo se ejecuta realmente su modelo. Cada función tendrá un nombre específico y todos los componentes deben tener esas funciones. Cualquier cosa específica de un componente debe ser accesible a través de esa API, de manera idéntica de componente a componente.

Su programa no debería intentar llamar a estas 'funciones definidas por el usuario'. En cambio, debería estar llamando a una función de "cálculo" de propósito general o algo así en cada componente, y el componente en sí se encarga de llamar a esa función y transformar su entrada en su salida. Las conexiones de entrada y salida son las abstracciones de esa función, eso es lo único que el programa debería ver.

En resumen, poco de esto es realmente específico de C ++, pero tendrá que implementar una especie de información de tipo de tiempo de ejecución, adaptada a su dominio de problema particular. Con cada función definida por la API, sabrá a qué nombres de funciones llamar en tiempo de ejecución, y conocerá los tipos de datos de cada una de esas llamadas, y solo usará la carga de la biblioteca dinámica antigua para hacerla. Esto vendrá con una buena cantidad de repetitivo, pero eso es solo parte de la vida.

Sin embargo, el único aspecto específico de C ++ que querrás tener en cuenta es que es mejor que tu API sea una API de C para que puedas usar diferentes compiladores para diferentes módulos, si los usuarios proporcionan sus propios módulos.

DirectShow es una API que hace todo lo que describí y puede ser un buen ejemplo para mirar.

whatsisname
fuente