Interagindo com classes C ++ do Swift

86

Tenho uma biblioteca significativa de classes escritas em C ++. Estou tentando fazer uso deles por meio de algum tipo de ponte dentro do Swift, em vez de reescrevê-los como código Swift. A principal motivação é que o código C ++ representa uma biblioteca central que é usada em várias plataformas. Efetivamente, estou apenas criando uma IU baseada em Swift para permitir que a funcionalidade principal funcione no OS X.

Existem outras perguntas: "Como faço para chamar uma função C ++ do Swift." Esta não é minha pergunta. Para fazer a ponte para uma função C ++, o seguinte funciona bem:

Defina um cabeçalho de ponte atravé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 */

O código Swift agora pode chamar funções diretamente

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

Minha pergunta é mais específica. Como posso instanciar e manipular uma classe C ++ de dentro do Swift? Não consigo encontrar nada publicado sobre isso.

David Hoelzer
fonte
8
Eu pessoalmente recorri a escrever arquivos de wrapper Objective-C ++ simples que expõem uma classe Objective-C que reproduz todas as chamadas C ++ relevantes e simplesmente as encaminha para uma instância mantida da classe C ++. No meu caso, o número de classes e chamadas C ++ é pequeno, portanto, não exige muito trabalho. Mas vou adiar a defesa disso como uma resposta na esperança de que alguém venha com algo melhor.
Tommy
1
Bem, é algo ... Vamos esperar para ver (e torcer).
David Hoelzer
Recebi uma sugestão via IRC para escrever uma classe de wrapper Swift que mantém um ponteiro void para o objeto C ++ real e expõe os métodos necessários que são, efetivamente, apenas passados ​​pela ponte C e o ponteiro para o objeto.
David Hoelzer
4
Como uma atualização, o Swift 3.0 foi lançado e, apesar das promessas anteriores, a interoperabilidade C ++ agora está marcada como "Fora do escopo".
David Hoelzer

Respostas:

61

Eu encontrei uma resposta perfeitamente administrável. O grau de limpeza que você gostaria que isso fosse depende inteiramente de quanto trabalho está disposto a fazer.

Primeiro, pegue sua classe C ++ e crie funções C "wrapper" para fazer a interface com ela. Por exemplo, se tivermos esta classe 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);
};

Em seguida, implementamos essas funções 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;
}

O cabeçalho da ponte contém:

#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 */

A partir do Swift, agora podemos instanciar o objeto e interagir com ele assim:

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!

Então, como você pode ver, a solução (que na verdade é bastante simples) é criar wrappers que irão instanciar um objeto e retornar um ponteiro para esse objeto. Isso pode então ser passado de volta para as funções de invólucro, que podem facilmente tratá-lo como um objeto em conformidade com aquela classe e chamar as funções-membro.

Tornando-o mais limpo

Embora seja um começo fantástico e prove que é completamente viável usar as classes C ++ existentes com uma ponte trivial, pode ser ainda mais limpo.

Limpar isso significaria simplesmente que removemos o UnsafeMutablePointer<Void>do meio de nosso código Swift e o encapsulamos em uma classe Swift. Essencialmente, usamos as mesmas funções de wrapper C / C ++, mas as interfaces com uma classe Swift. A classe Swift mantém a referência do objeto e essencialmente apenas passa todas as chamadas de referência de método e atributo através da ponte para o objeto C ++!

Feito isso, todo o código de bridging é completamente encapsulado na classe Swift. Mesmo que ainda estejamos usando uma ponte C, estamos efetivamente usando objetos C ++ de forma transparente, sem ter que recorrer a recodificá-los em Objective-C ou Objective-C ++.

David Hoelzer
fonte
12
Há algumas questões importantes perdidas aqui. O primeiro é que o destruidor nunca é chamado, e o objeto vaza. A segunda é que as exceções podem causar alguns problemas desagradáveis.
Michael Anderson
2
Abordei vários problemas em minha resposta a esta pergunta stackoverflow.com/questions/2045774/…
Michael Anderson
2
@DavidHoelzer, eu só queria enfatizar essa ideia porque alguns desenvolvedores tentarão encapsular uma biblioteca C ++ inteira e usá-la como foi escrita em Swift. Você deu um ótimo exemplo de como "instanciar e manipular uma classe C ++ de dentro do Swift"! E eu só queria acrescentar que essa técnica deve ser usada com cautela! Obrigado pelo seu exemplo!
Nicolai Nita
1
Não pense que isso é "perfeito". Acho que é quase o mesmo que escrever um wrapper Objective-C e depois usá-lo no Swift.
Bagusflyer
1
@Bagusflyer com certeza ... exceto que não é um invólucro Objectivo-C.
David Hoelzer
11

O Swift não tem interoperabilidade C ++ atualmente. É uma meta de longo prazo, mas é muito improvável que aconteça em um futuro próximo.

Catfish_Man
fonte
25
Chamar "use C wrappers" como uma forma de "interoperabilidade C ++" é esticar um pouco o termo
Catfish_Man
6
Talvez, mas usá-los para permitir que você instancie uma classe C ++ e, em seguida, chame métodos sobre ela, torna-o útil e satisfaz a questão.
David Hoelzer
2
Na verdade, não é mais uma meta de longo prazo, mas, em vez disso, está marcada como "Fora do escopo" no roteiro.
David Hoelzer
4
usar c-wrappers é a abordagem padrão para quase toda a interoperabilidade C ++ para qualquer linguagem, python, java, etc.
Andrew Paul Simmons
8

Além de sua própria solução, existe outra maneira de fazer isso. Você pode chamar ou escrever diretamente o código C ++ em object-c ++.

Portanto, você pode criar um wrapper objetivo-C ++ no topo de seu código C ++ e criar uma interface adequada.

Em seguida, chame o código objetivo-C ++ do seu código swift. Para poder escrever código C ++ objetivo, pode ser necessário renomear a extensão do arquivo de .m para .mm

Não se esqueça de liberar memória alocada por seus objetos C ++ quando apropriado.

Ahmed
fonte
6

Como outra resposta mencionada, usar ObjC ++ para interagir é muito mais fácil. Basta nomear seus arquivos .mm em vez de .m e xcode / clang, e você terá acesso a c ++ nesse arquivo.

Observe que ObjC ++ não oferece suporte à herança de C ++. Se você quiser criar uma subclasse de uma classe c ++ em ObjC ++, não pode. Você terá que escrever a subclasse em C ++ e envolvê-la em uma classe ObjC ++.

Em seguida, use o cabeçalho de ponte que você normalmente usaria para chamar objc do swift.

nnrales
fonte
2
Você pode ter perdido o ponto. As interfaces são necessárias porque temos um aplicativo C ++ multiplataforma muito grande que agora também oferecemos no OS X. O Objective C ++ realmente não é uma opção para todo o projeto.
David Hoelzer,
Eu não tenho certeza se entendo você. Você só precisa do ObjC ++ quando quiser que uma chamada vá entre swift e c ++ ou vice-versa. Apenas os limites precisam ser escritos em objc ++, não o projeto inteiro.
nnrales
1
Sim, eu repassei sua pergunta. Você parece estar procurando manipular diretamente o C ++ a partir do swift. Eu não sei fazer isso.
nnrales
1
Isso é o que minha resposta postada realiza. As funções C permitem que você passe os objetos para dentro e para fora como blocos arbitrários de memória e para chamar métodos em ambos os lados.
David Hoelzer
1
@DavidHoelzer Parece legal, mas você não está complicando o código dessa forma? Eu acho que é subjetivo. O invólucro ObjC ++ parece mais limpo para mim.
nnrales
5

Você pode usar o Scapix Language Bridge para ligar automaticamente o C ++ ao Swift (entre outras linguagens). Código de ponte gerado automaticamente em tempo real, diretamente de arquivos de cabeçalho C ++. Aqui está um exemplo :

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
fonte
Interessante. Parece que você acabou de lançar isso pela primeira vez em março de 2019.
David Hoelzer
@ boris-rasin Não consigo encontrar c ++ direto para ponte rápida no exemplo. Você tem esse recurso em planos?
sage444
2
Sim, não há uma ponte direta de C ++ para Swift no momento. Mas a ponte C ++ para ObjC funciona perfeitamente com o Swift (por meio da ponte ObjC para Swift da Apple). Estou trabalhando na ponte direta agora (fornecerá melhor desempenho em alguns casos).
Boris Rasin
1
Ah, sim, você está totalmente certo, mas em um projeto simples e rápido no Linux, esse não é o caso. Estamos ansiosos para ver sua implementação.
sage444