Interactuar con clases de C ++ de Swift

86

Tengo una biblioteca importante de clases escritas en C ++. Estoy tratando de hacer uso de ellos a través de algún tipo de puente dentro de Swift en lugar de reescribirlos como código Swift. La motivación principal es que el código C ++ representa una biblioteca central que se utiliza en múltiples plataformas. Efectivamente, solo estoy creando una interfaz de usuario basada en Swift para permitir que la funcionalidad principal funcione en OS X.

Hay otras preguntas que plantean: "¿Cómo llamo a una función C ++ desde Swift?". Ésta no es mi pregunta. Para pasar a una función de C ++, lo siguiente funciona bien:

Definir un encabezado puente a través de "C"

#ifndef ImageReader_hpp
#define ImageReader_hpp

#ifdef __cplusplus
extern "C" {
#endif

    const char *hexdump(char *filename);
    const char *imageType(char *filename);

#ifdef __cplusplus
}
#endif

#endif /* ImageReader_hpp */

El código Swift ahora puede llamar a funciones directamente

let type = String.fromCString(imageType(filename))
let dump = String.fromCString(hexdump(filename))

Mi pregunta es más específica. ¿Cómo puedo crear una instancia y manipular una clase C ++ desde Swift? Parece que no puedo encontrar nada publicado sobre esto.

David Hoelzer
fuente
8
Personalmente, he recurrido a escribir archivos de contenedor de Objective-C ++ simples que exponen una clase Objective-C que reproduce todas las llamadas de C ++ relevantes y simplemente las reenvía a una instancia retenida de la clase C ++. En mi caso, la cantidad de clases y llamadas de C ++ es pequeña, por lo que no es particularmente laboriosa. Pero me abstendré de defender esto como una respuesta con la esperanza de que alguien haya encontrado algo mejor.
Tommy
1
Bueno, es algo ... Esperemos y veremos (y esperemos).
David Hoelzer
Recibí una sugerencia a través de IRC para escribir una clase contenedora Swift que mantiene un puntero vacío al objeto C ++ real y expone los métodos requeridos que, efectivamente, simplemente se pasan a través del puente C y el puntero al objeto.
David Hoelzer
4
Como actualización de esto, Swift 3.0 ya está disponible y, a pesar de las promesas anteriores, la interoperabilidad de C ++ ahora está marcada como "Fuera de alcance".
David Hoelzer

Respuestas:

61

He elaborado una respuesta perfectamente manejable. Lo limpio que le gustaría que fuera esto se basa completamente en la cantidad de trabajo que esté dispuesto a hacer.

Primero, tome su clase C ++ y cree funciones de "envoltura" de C para interactuar con ella. Por ejemplo, si tenemos esta clase C ++:

class MBR {
    std::string filename;

public:
    MBR (std::string filename);
    const char *hexdump();
    const char *imageType();
    const char *bootCode();
    const char *partitions();
private:
    bool readFile(unsigned char *buffer, const unsigned int length);
};

Luego implementamos estas funciones de C ++:

#include "MBR.hpp"

using namespace std;
const void * initialize(char *filename)
{
    MBR *mbr = new MBR(filename);

    return (void *)mbr;
}

const char *hexdump(const void *object)
{
    MBR *mbr;
    static char retval[2048];

    mbr = (MBR *)object;
    strcpy(retval, mbr -> hexdump());
    return retval;
}

const char *imageType(const void *object)
{
    MBR *mbr;
    static char retval[256];

    mbr = (MBR *)object;
    strcpy(retval, mbr -> imageType());
    return retval;
}

El encabezado del puente luego contiene:

#ifndef ImageReader_hpp
#define ImageReader_hpp

#ifdef __cplusplus
extern "C" {
#endif

    const void *initialize(char *filename);
    const char *hexdump(const void *object);
    const char *imageType(const void *object);

#ifdef __cplusplus
}
#endif

#endif /* ImageReader_hpp */

Desde Swift, ahora podemos instanciar el objeto e interactuar con él así:

let cppObject = UnsafeMutablePointer<Void>(initialize(filename))
let type = String.fromCString(imageType(cppObject))
let dump = String.fromCString(hexdump(cppObject))                
self.imageTypeLabel.stringValue = type!
self.dumpDisplay.stringValue = dump!

Entonces, como puede ver, la solución (que en realidad es bastante simple) es crear contenedores que crearán una instancia de un objeto y devolverán un puntero a ese objeto. Esto luego se puede pasar de nuevo a las funciones contenedoras que pueden tratarlo fácilmente como un objeto conforme a esa clase y llamar a las funciones miembro.

Haciéndolo más limpio

Si bien este es un comienzo fantástico y demuestra que es completamente factible usar clases de C ++ existentes con un puente trivial, puede ser aún más limpio.

Limpiar esto simplemente significaría que eliminamos el UnsafeMutablePointer<Void>del medio de nuestro código Swift y lo encapsulamos en una clase Swift. Esencialmente, usamos las mismas funciones contenedoras de C / C ++ pero las interconectamos con una clase Swift. La clase Swift mantiene la referencia del objeto y esencialmente solo pasa todas las llamadas de referencia de método y atributo a través del puente al objeto C ++.

Una vez hecho esto, todo el código puente está completamente encapsulado en la clase Swift. Aunque todavía estamos usando un puente C, estamos usando efectivamente objetos C ++ de forma transparente sin tener que recurrir a recodificarlos en Objective-C o Objective-C ++.

David Hoelzer
fuente
12
Aquí hay un par de cuestiones importantes que se han perdido. El primero es que nunca se llama al destructor y se filtra el objeto. La segunda es que las excepciones pueden causar problemas desagradables.
Michael Anderson
2
Cubrí un montón de problemas en mi respuesta a esta pregunta stackoverflow.com/questions/2045774/…
Michael Anderson
2
@DavidHoelzer, solo quería enfatizar esta idea porque algunos desarrolladores intentarán empaquetar una biblioteca C ++ completa y usarla tal como se escribió en Swift. ¡Diste un gran ejemplo de cómo "crear instancias y manipular una clase C ++ desde Swift"! ¡Y solo quería agregar que esta técnica debe usarse con precaución! ¡Gracias por tu ejemplo!
Nicolai Nita
1
No crea que esto es "perfecto". Creo que es casi lo mismo que escribir un contenedor Objective-C y luego usarlo en Swift.
Bagusflyer
1
@Bagusflyer seguro ... excepto que no es una envoltura de objetivo-C en absoluto.
David Hoelzer
11

Swift no tiene interoperabilidad C ++ actualmente. Es un objetivo a largo plazo, pero es muy poco probable que suceda en un futuro próximo.

Hombre_Bagre
fuente
25
Llamar a "usar envoltorios de C" como una forma de "interoperabilidad de C ++" está estirando el término bastante
Catfish_Man
6
Quizás, pero usarlos para permitirle crear una instancia de una clase C ++ y luego llamar a métodos sobre ella lo hace útil y satisface la pregunta.
David Hoelzer
2
De hecho, ya no es un objetivo a largo plazo, sino que está marcado como "Fuera de alcance" en la hoja de ruta.
David Hoelzer
4
el uso de c-wrappers es el enfoque estándar para casi toda la interoperabilidad de C ++ con cualquier lenguaje, python, java, etc.
Andrew Paul Simmons
8

Además de su propia solución, hay otra forma de hacerlo. Puede llamar o escribir directamente código C ++ en aim-c ++.

Por lo tanto, puede crear un contenedor de objetivo-C ++ sobre su código C ++ y crear una interfaz adecuada.

Luego llame al código objetivo-C ++ desde su código rápido. Para poder escribir código objetivo-C ++, es posible que deba cambiar el nombre de la extensión del archivo de .m a .mm

No olvide liberar la memoria asignada por sus objetos C ++ cuando sea adecuado.

Ahmed
fuente
6

Como se mencionó en otra respuesta, usar ObjC ++ para interactuar es mucho más fácil. Simplemente nombre sus archivos .mm en lugar de .my xcode / clang, le da acceso a c ++ en ese archivo.

Tenga en cuenta que ObjC ++ no admite la herencia de C ++. Si desea subclasificar una clase de C ++ en ObjC ++, no puede. Tendrá que escribir la subclase en C ++ y envolverla alrededor de una clase ObjC ++.

Luego use el encabezado de puente que normalmente usaría para llamar a objc desde swift.

nrales
fuente
2
Puede que hayas perdido el punto. Las interfaces son necesarias porque tenemos una aplicación C ++ multiplataforma muy grande que ahora también admitimos en OS X. Objective C ++ realmente no es una opción para todo el proyecto.
David Hoelzer
No estoy seguro de entenderte. Solo necesita ObjC ++ cuando desee que una llamada vaya entre swift y c ++ o viceversa. Solo los límites deben escribirse en objc ++, no en todo el proyecto.
nnrales
1
Sí, repasé tu pregunta. Parece que busca manipular directamente C ++ desde swift. No sé cómo hacer eso.
nnrales
1
Eso es lo que logra mi respuesta publicada. Las funciones C le permiten pasar los objetos dentro y fuera como pedazos arbitrarios de memoria y llamar a métodos en ambos lados.
David Hoelzer
1
@DavidHoelzer Creo que es genial, pero ¿no estás haciendo el código más complicado de esta manera? Supongo que es subjetivo. La envoltura de ObjC ++ me parece más limpia.
nnrales
5

Puede utilizar Scapix Language Bridge para conectar automáticamente C ++ a Swift (entre otros idiomas). Código puente generado automáticamente sobre la marcha directamente desde archivos de encabezado C ++. He aquí un ejemplo :

C ++:

#include <scapix/bridge/object.h>

class contact : public scapix::bridge::object<contact>
{
public:
    std::string name();
    void send_message(const std::string& msg, std::shared_ptr<contact> from);
    void add_tags(const std::vector<std::string>& tags);
    void add_friends(std::vector<std::shared_ptr<contact>> friends);
};

Rápido:

class ViewController: UIViewController {
    func send(friend: Contact) {
        let c = Contact()

        contact.sendMessage("Hello", friend)
        contact.addTags(["a","b","c"])
        contact.addFriends([friend])
    }
}
Boris Rasin
fuente
Interesante. Parece que acaba de publicar esto por primera vez en marzo de 2019.
David Hoelzer
@ boris-rasin No puedo encontrar C ++ directo a Swift Bridge en el ejemplo. ¿Tiene esta función en los planes?
sage444
2
Sí, no hay un puente directo de C ++ a Swift en este momento. Pero el puente de C ++ a ObjC funciona perfectamente para Swift (a través del puente de ObjC a Swift de Apple). Estoy trabajando en el puente directo en este momento (proporcionará un mejor rendimiento en algunos casos).
Boris Rasin
1
Oh, sí, tiene toda la razón, pero en un proyecto simple y rápido en Linux, este no es el caso. Esperamos ver su implementación.
sage444