Desarrollo de la API contenedora de C para código C ++ orientado a objetos

81

Estoy buscando desarrollar un conjunto de API de C que se adapte a nuestras API de C ++ existentes para acceder a nuestra lógica central (escrita en C ++ orientado a objetos). Básicamente, será una API adhesiva que permitirá que otros lenguajes puedan utilizar nuestra lógica C ++. ¿Cuáles son algunos buenos tutoriales, libros o mejores prácticas que presentan los conceptos involucrados en envolver C alrededor de C ++ orientado a objetos?

el actor activo
fuente
4
echa un vistazo a la fuente de zeromq para inspirarte. La biblioteca está escrita actualmente en C ++ y tiene enlaces C. zeromq.org
Hassan Syed
1
Relacionado (o incluso un duplicado): Envolviendo API de clase C ++ para consumo de C
usuario

Respuestas:

69

Esto no es demasiado difícil de hacer a mano, pero dependerá del tamaño de su interfaz. Los casos en los que lo hice fueron para permitir el uso de nuestra biblioteca C ++ desde el código C puro y, por lo tanto, SWIG no fue de mucha ayuda. (Bueno, tal vez SWIG pueda usarse para hacer esto, pero no soy un gurú de SWIG y no parecía trivial)

Todo lo que terminamos haciendo fue:

  1. Cada objeto pasa en C por un asa opaca.
  2. Los constructores y destructores están envueltos en funciones puras
  3. Las funciones miembro son funciones puras.
  4. Otras incorporaciones se asignan a equivalentes de C cuando es posible.

Entonces, una clase como esta (encabezado C ++)

class MyClass
{
  public:
  explicit MyClass( std::string & s );
  ~MyClass();
  int doSomething( int j );
}

Se asignaría a una interfaz C como esta (encabezado C):

struct HMyClass; // An opaque type that we'll use as a handle
typedef struct HMyClass HMyClass;
HMyClass * myStruct_create( const char * s );
void myStruct_destroy( HMyClass * v );
int myStruct_doSomething( HMyClass * v, int i );

La implementación de la interfaz se vería así (fuente C ++)

#include "MyClass.h"

extern "C" 
{
  HMyClass * myStruct_create( const char * s )
  {
    return reinterpret_cast<HMyClass*>( new MyClass( s ) );
  }
  void myStruct_destroy( HMyClass * v )
  {
    delete reinterpret_cast<MyClass*>(v);
  }
  int myStruct_doSomething( HMyClass * v, int i )
  {
    return reinterpret_cast<MyClass*>(v)->doSomething(i);
  }
}

Derivamos nuestro identificador opaco de la clase original para evitar la necesidad de conversión, y (Esto no parece funcionar con mi compilador actual). Tenemos que convertir el identificador en una estructura, ya que C no admite clases.

Eso nos da la interfaz C básica. Si desea un ejemplo más completo que muestre una forma en que puede integrar el manejo de excepciones, puede probar mi código en github: https://gist.github.com/mikeando/5394166

La parte divertida ahora es asegurarse de que obtenga todas las bibliotecas de C ++ necesarias vinculadas correctamente a su biblioteca más grande. Para gcc (o clang) eso significa simplemente hacer la etapa de enlace final usando g ++.

Michael Anderson
fuente
11
Te recomiendo que uses algo más que void, por ejemplo, una estructura anónima en lugar de un void * para el objeto devuelto. Esto puede proporcionar algún tipo de seguridad para las asas devueltas. Consulte stackoverflow.com/questions/839765/… para obtener más información al respecto.
Laserallan
3
Estoy de acuerdo con Laserallan y he refactorizado mi código en consecuencia
Michael Anderson
2
@Mike Weller El nuevo y eliminar dentro del bloque "C" externo está bien. La "C" externa sólo afecta la alteración del nombre. El compilador de C nunca ve ese archivo, solo el encabezado.
Michael Anderson
2
También me perdí una typedef necesaria para compilar todo en C. La extraña estructura typdef Foo Foo; "cortar a tajos". El código se actualizó
Michael Anderson
5
@MichaelAnderson, hay dos errores tipográficos en sus funciones myStruct_destroyy myStruct_doSomething. Debería ser reinterpret_cast<MyClass*>(v).
firegurafiku
17

Creo que la respuesta de Michael Anderson está en el camino correcto, pero mi enfoque sería diferente. Tienes que preocuparte por una cosa más: Excepciones. Las excepciones no forman parte de la ABI de C, por lo que no puede permitir que las excepciones pasen del código C ++. Entonces, su encabezado se verá así:

#ifdef __cplusplus
extern "C"
{
#endif
    void * myStruct_create( const char * s );
    void myStruct_destroy( void * v );
    int myStruct_doSomething( void * v, int i );
#ifdef __cplusplus
}
#endif

Y el archivo .cpp de su contenedor se verá así:

void * myStruct_create( const char * s ) {
    MyStruct * ms = NULL;
    try { /* The constructor for std::string may throw */
        ms = new MyStruct(s);
    } catch (...) {}
    return static_cast<void*>( ms );
}

void myStruct_destroy( void * v ) {
    MyStruct * ms = static_cast<MyStruct*>(v);
    delete ms;
}

int myStruct_doSomething( void * v, int i ) {
    MyStruct * ms = static_cast<MyStruct*>(v);
    int ret_value = -1; /* Assuming that a negative value means error */
    try {
        ret_value = ms->doSomething(i);
    } catch (...) {}
    return ret_value;
}

Aún mejor: si sabe que todo lo que necesita es una única instancia de MyStruct, no se arriesgue a lidiar con punteros nulos que se pasen a su API. En su lugar, haz algo como esto:

static MyStruct * _ms = NULL;

int myStruct_create( const char * s ) {
    int ret_value = -1; /* error */
    try { /* The constructor for std::string may throw */
        _ms = new MyStruct(s);
        ret_value = 0; /* success */
    } catch (...) {}
    return ret_value;
}

void myStruct_destroy() {
    if (_ms != NULL) {
        delete _ms;
    }
}

int myStruct_doSomething( int i ) {
    int ret_value = -1; /* Assuming that a negative value means error */
    if (_ms != NULL) {
        try {
            ret_value = _ms->doSomething(i);
        } catch (...) {}
    }
    return ret_value;
}

Esta API es mucho más segura.

Pero, como mencionó Michael, la vinculación puede volverse bastante complicada.

Espero que esto ayude

figurassa
fuente
2
Para obtener más información sobre el manejo de excepciones para este caso, eche un vistazo al siguiente hilo: stackoverflow.com/questions/847279/…
Laserallan
2
Cuando sé que mi biblioteca de C ++ también tendrá una API de C, encapsulo un código de error de API int dentro de mi clase base de excepción. Es más fácil saber en el sitio de lanzamiento cuál es la condición de error exacta y proporcionar un código de error muy específico. Los "envoltorios" try-catch en las funciones externas de la API C simplemente necesitan recuperar el código de error y devolverlo a la persona que llama. Para otras excepciones estándar de la biblioteca, consulte el enlace de Laserallan.
Emile Cormier
2
catch (...) {} es pura maldad sin adulterar. Lo único que lamento es que solo pueda votar en contra una vez.
Terry Mahaffey
2
@Terry Mahaffey Estoy absolutamente de acuerdo contigo en que es malvado. Lo mejor es hacer lo que ha sugerido Emile. Pero si debe garantizar que el código envuelto nunca se lanzará, no tiene más remedio que poner una captura (...) al final de todas las otras capturas identificadas. Este es el caso porque la biblioteca que está empaquetando puede estar mal documentada. No hay construcciones de C ++ que pueda usar para hacer cumplir que solo se puede lanzar un conjunto de excepciones. ¿Cuál es el menor de dos males? catch (...) o arriesgarse a un bloqueo en tiempo de ejecución cuando el código envuelto intenta lanzarse a la persona que llama C?
figurassa
1
catch (...) {std :: terminate (); } es aceptable. catch (...) {} es un posible agujero de seguridad
Terry Mahaffey
10

No es difícil exponer el código C ++ a C, solo use el patrón de diseño Facade

Supongo que su código C ++ está integrado en una biblioteca, todo lo que necesita hacer es crear un módulo C en su biblioteca C ++ como una fachada a su biblioteca junto con un archivo de encabezado C puro. El módulo C llamará a las funciones relevantes de C ++

Una vez que lo haga, sus aplicaciones y biblioteca C tendrán acceso completo a la API de C que expuso.

por ejemplo, aquí hay un módulo de fachada de muestra

#include <libInterface.h>
#include <objectedOrientedCppStuff.h>

int doObjectOrientedStuff(int *arg1, int arg2, char *arg3) {
      Object obj = ObjectFactory->makeCppObj(arg3); // doing object oriented stuff here
      obj->doStuff(arg2);
      return obj->doMoreStuff(arg1);
   }

luego expone esta función C como su API y puede usarla libremente como una C lib sin preocuparse por

// file name "libIntrface.h"
extern int doObjectOrientedStuff(int *, int, char*);

Obviamente, este es un ejemplo artificial, pero esta es la forma más fácil de exponer una biblioteca C ++ a C

hhafez
fuente
Hola @hhafez, ¿tienes un ejemplo simple de hola mundo? ¿Uno con cuerdas?
Jason Foglia
para un chico que no es cpp esto es encantador
Nicklas Avén
6

Creo que podría obtener algunas ideas sobre la dirección y / o posiblemente utilizar directamente SWIG . Creo que repasar algunos de los ejemplos al menos te daría una idea de qué tipo de cosas debes considerar al encapsular una API en otra. El ejercicio puede resultar beneficioso.

SWIG es una herramienta de desarrollo de software que conecta programas escritos en C y C ++ con una variedad de lenguajes de programación de alto nivel. SWIG se utiliza con diferentes tipos de lenguajes, incluidos los lenguajes de scripting comunes como Perl, PHP, Python, Tcl y Ruby. La lista de lenguajes admitidos también incluye lenguajes que no son de scripting como C #, Common Lisp (CLISP, Allegro CL, CFFI, UFFI), Java, Lua, Modula-3, OCAML, Octave y R. También varias implementaciones de Scheme interpretadas y compiladas ( Guile, MzScheme, Chicken) son compatibles. SWIG se usa más comúnmente para crear entornos de programación interpretados o compilados de alto nivel, interfaces de usuario y como una herramienta para probar y crear prototipos de software C / C ++. SWIG también puede exportar su árbol de análisis en forma de expresiones XML y Lisp. SWIG se puede utilizar, distribuir,

harschware
fuente
2
SWIG acaba de terminar, si todo lo que quiere hacer es hacer que una biblioteca de C ++ esté disponible en C.
hhafez
1
Esa es una opinión y no contiene comentarios realmente útiles. SWIG ayudaría si el código original: Cambia rápidamente, no hay recursos C ++ para mantenerlo y solo recursos C disponibles y si el desarrollador desea automatizar la generación de API C. Estas son razones comunes y ciertamente válidas para usar SWIG.
user1363990
5

Simplemente reemplace el concepto de un objeto con un void *(a menudo denominado tipo opaco en las bibliotecas orientadas a C) y reutilice todo lo que sabe de C ++.

Hassan Syed
fuente
2

Creo que usar SWIG es la mejor respuesta ... no solo evita reinventar la rueda, sino que es confiable y también promueve una continuidad en el desarrollo en lugar de solucionar el problema.

Los problemas de alta frecuencia deben abordarse mediante una solución a largo plazo.

danbo
fuente