Wywołanie metody Objective-C z funkcji składowej C ++?

111

Mam klasę ( EAGLView), która C++bez problemu wywołuje funkcję składową klasy. Teraz problem polega na tym, że muszę wywołać w tej C++klasie a, objective-C function [context renderbufferStorage:GL_RENDERBUFFER fromDrawable:(CAEAGLLayer*)self.layer];czego nie mogę zrobić w C++składni.

Mógłbym zawinąć to Objective-Cwywołanie do tej samej Objective-Cklasy, która na początku nazywała się klasą C ++, ale potem muszę jakoś wywołać tę metodę z C++i nie mogę dowiedzieć się, jak to zrobić.

Próbowałem podać wskaźnik EAGLViewobiektu do funkcji składowej C ++ i dołączyć „ EAGLView.h” do C++nagłówka mojej klasy, ale otrzymałem 3999 błędów.

Więc .. jak mam to zrobić? Przykład byłby fajny ... Znalazłem tylko czyste Cprzykłady robienia tego.

juvenis
źródło

Odpowiedzi:

204

Możesz mieszać C ++ z Objective-C, jeśli robisz to ostrożnie. Jest kilka zastrzeżeń, ale ogólnie można je mieszać. Jeśli chcesz je zachować osobno, możesz ustawić standardową funkcję opakowującą C, która daje obiektowi Objective-C użyteczny interfejs w stylu C z kodu innego niż Objective-C (wybierz lepsze nazwy dla swoich plików, wybrałem te nazwy dla szczegółowości):

MyObject-C-Interface.h

#ifndef __MYOBJECT_C_INTERFACE_H__
#define __MYOBJECT_C_INTERFACE_H__

// This is the C "trampoline" function that will be used
// to invoke a specific Objective-C method FROM C++
int MyObjectDoSomethingWith (void *myObjectInstance, void *parameter);
#endif

MyObject.h

#import "MyObject-C-Interface.h"

// An Objective-C class that needs to be accessed from C++
@interface MyObject : NSObject
{
    int someVar;
}

// The Objective-C member function you want to call from C++
- (int) doSomethingWith:(void *) aParameter;
@end

MyObject.mm

#import "MyObject.h"

@implementation MyObject

// C "trampoline" function to invoke Objective-C method
int MyObjectDoSomethingWith (void *self, void *aParameter)
{
    // Call the Objective-C method using Objective-C syntax
    return [(id) self doSomethingWith:aParameter];
}

- (int) doSomethingWith:(void *) aParameter
{
    // The Objective-C function you wanted to call from C++.
    // do work here..
    return 21 ; // half of 42
}
@end

MyCPPClass.cpp

#include "MyCPPClass.h"
#include "MyObject-C-Interface.h"

int MyCPPClass::someMethod (void *objectiveCObject, void *aParameter)
{
    // To invoke an Objective-C method from C++, use
    // the C trampoline function
    return MyObjectDoSomethingWith (objectiveCObject, aParameter);
}

Funkcja opakowania nie musi znajdować się w tym samym .mpliku co klasa Objective-C, ale plik, w którym istnieje, musi być skompilowany jako kod Objective-C . Nagłówek, który deklaruje funkcję opakowania, musi być zawarty zarówno w kodzie CPP, jak i Objective-C.

(UWAGA: jeśli plik implementacji Objective-C ma rozszerzenie „.m”, nie będzie łączył się z Xcode. Rozszerzenie „.mm” mówi Xcode, aby oczekiwał kombinacji Objective-C i C ++, tj. Objective-C ++. )


Możesz zaimplementować powyższe w sposób zorientowany obiektowo, używając idiomu PIMPL . Implementacja jest tylko trochę inna. Krótko mówiąc, umieszczasz funkcje opakowujące (zadeklarowane w „MyObject-C-Interface.h”) wewnątrz klasy z (prywatnym) wskaźnikiem void do instancji MyClass.

MyObject-C-Interface.h (PIMPL)

#ifndef __MYOBJECT_C_INTERFACE_H__
#define __MYOBJECT_C_INTERFACE_H__

class MyClassImpl
{
public:
    MyClassImpl ( void );
    ~MyClassImpl( void );

    void init( void );
    int  doSomethingWith( void * aParameter );
    void logMyMessage( char * aCStr );

private:
    void * self;
};

#endif

Zwróć uwagę, że metody opakowujące nie wymagają już wskaźnika void do instancji MyClass; jest teraz prywatnym członkiem MyClassImpl. Metoda init służy do tworzenia instancji MyClass;

MyObject.h (PIMPL)

#import "MyObject-C-Interface.h"

@interface MyObject : NSObject
{
    int someVar;
}

- (int)  doSomethingWith:(void *) aParameter;
- (void) logMyMessage:(char *) aCStr;

@end

MyObject.mm (PIMPL)

#import "MyObject.h"

@implementation MyObject

MyClassImpl::MyClassImpl( void )
    : self( NULL )
{   }

MyClassImpl::~MyClassImpl( void )
{
    [(id)self dealloc];
}

void MyClassImpl::init( void )
{    
    self = [[MyObject alloc] init];
}

int MyClassImpl::doSomethingWith( void *aParameter )
{
    return [(id)self doSomethingWith:aParameter];
}

void MyClassImpl::logMyMessage( char *aCStr )
{
    [(id)self doLogMessage:aCStr];
}

- (int) doSomethingWith:(void *) aParameter
{
    int result;

    // ... some code to calculate the result

    return result;
}

- (void) logMyMessage:(char *) aCStr
{
    NSLog( aCStr );
}

@end

Zwróć uwagę, że MyClass jest tworzony przez wywołanie MyClassImpl :: init. Możesz utworzyć wystąpienie MyClass w konstruktorze MyClassImpl, ale generalnie nie jest to dobry pomysł. Instancja MyClass jest niszczona z destruktora MyClassImpl. Podobnie jak w przypadku implementacji w stylu C, metody opakowujące po prostu odnoszą się do odpowiednich metod MyClass.

MyCPPClass.h (PIMPL)

#ifndef __MYCPP_CLASS_H__
#define __MYCPP_CLASS_H__

class MyClassImpl;

class MyCPPClass
{
    enum { cANSWER_TO_LIFE_THE_UNIVERSE_AND_EVERYTHING = 42 };
public:
    MyCPPClass ( void );
    ~MyCPPClass( void );

    void init( void );
    void doSomethingWithMyClass( void );

private:
    MyClassImpl * _impl;
    int           _myValue;
};

#endif

MyCPPClass.cpp (PIMPL)

#include "MyCPPClass.h"
#include "MyObject-C-Interface.h"

MyCPPClass::MyCPPClass( void )
    : _impl ( NULL )
{   }

void MyCPPClass::init( void )
{
    _impl = new MyClassImpl();
}

MyCPPClass::~MyCPPClass( void )
{
    if ( _impl ) { delete _impl; _impl = NULL; }
}

void MyCPPClass::doSomethingWithMyClass( void )
{
    int result = _impl->doSomethingWith( _myValue );
    if ( result == cANSWER_TO_LIFE_THE_UNIVERSE_AND_EVERYTHING )
    {
        _impl->logMyMessage( "Hello, Arthur!" );
    }
    else
    {
        _impl->logMyMessage( "Don't worry." );
    }
}

Masz teraz dostęp do wywołań MyClass poprzez prywatną implementację MyClassImpl. Takie podejście może być korzystne, jeśli tworzysz aplikację przenośną; możesz po prostu zamienić implementację MyClass na specyficzną dla drugiej platformy ... ale szczerze mówiąc, to, czy jest to lepsza implementacja, jest bardziej kwestią gustu i potrzeb.

dreamlax
źródło
6
Cześć, próbowałem, ale pojawia się błąd połączenia z informacją, że nie znaleziono symbolu (ów). tj. nie może znaleźć MyObjectDoSomethingWith. jakieś pomysły?
3
Być może będziesz musiał dodać extern "C"przedint MyObjectDoSomethingWith
dreamlax
2
próbowałem już tego, nie działa i ma to sens, ponieważ zewnętrzne „C” jest używane, gdy chcemy wywołać funkcję C ++ z C, w tym przypadku wywołujemy funkcję C z C ++, nie?
6
Ponadto, w jaki sposób inicjowany jest obiekt objectiveCObject w MyCPPClass.cpp?
Raffi Khatchadourian
2
Niesamowity @dreamlax, który się teraz kompiluje, ale nie wiem, jak to nazwać „someMethod”. Do jakich parametrów należy dodać: int MyCPPClass :: someMethod (void * objectiveCObject, void * aParameter) ???
the_moon,
15

Możesz skompilować swój kod jako Objective-C ++ - najprostszym sposobem jest zmiana nazwy pliku .cpp na .mm. Następnie skompiluje się poprawnie, jeśli dołączysz EAGLView.h(otrzymywało się tak wiele błędów, ponieważ kompilator C ++ nie zrozumiał żadnego ze słów kluczowych specyficznych dla celu C) i możesz (w większości) mieszać Objective-C i C ++, jakkolwiek lubić.

Jesse Beder
źródło
Czy otrzymujesz wszystkie te błędy kompilatora w tym pliku C ++, czy są one w innym pliku C ++, który zawiera ten nagłówek C ++?
Jesse Beder
Wygląda na to, że nie mogę dołączyć EAGLView.h do pliku nagłówkowego C ++, ponieważ wtedy z jakiegoś powodu oczekuje, że kod Objective C to C ++ i nie rozumie @ + innych symboli
juvenis
12

Najłatwiejszym rozwiązaniem jest po prostu powiedzenie Xcode, aby skompilował wszystko jako Objective C ++.

Ustaw projekt lub ustawienia docelowe dla Skompiluj źródła jako cel C ++ i ponownie skompiluj.

Wtedy możesz używać C ++ lub Objective C wszędzie, na przykład:

void CPPObject::Function( ObjectiveCObject* context, NSView* view )
{
   [context renderbufferStorage:GL_RENDERBUFFER fromDrawable:(CAEAGLLayer*)view.layer]
}

Ma to taki sam wpływ, jak zmiana nazw wszystkich plików źródłowych z .cpp lub .m na .mm.

Ma to dwie drobne wady: clang nie może analizować kodu źródłowego C ++; jakiś stosunkowo dziwny kod w C nie kompiluje się w C ++.

Peter N Lewis
źródło
Jestem trochę ciekawy, czy kiedy kompilujesz wszystko jako Objective-C ++, otrzymujesz ostrzeżenia dotyczące używania rzutów w stylu C i / lub innych ostrzeżeń specyficznych dla C ++ o poprawnym kodzie w stylu C?
dreamlax
Jasne, programujesz w C ++, więc oczekuje się, że będziesz się odpowiednio zachowywać - ale z reguły C ++ jest lepszym C niż C, nawet jeśli nigdy nie utworzysz klasy. Nie pozwoli ci robić głupich rzeczy i pozwala robić fajne rzeczy (jak lepsze stałe, wyliczenia i tym podobne). Nadal możesz rzutować to samo (np. (CFFloat) x).
Peter N Lewis
10

Krok 1

Utwórz plik C celu (plik .m) i odpowiadający mu plik nagłówkowy.

// Plik nagłówkowy (nazywamy go „ObjCFunc.h”)

#ifndef test2_ObjCFunc_h
#define test2_ObjCFunc_h
@interface myClass :NSObject
-(void)hello:(int)num1;
@end
#endif

// Odpowiedni plik celu C (nazywamy go „ObjCFunc.m”)

#import <Foundation/Foundation.h>
#include "ObjCFunc.h"
@implementation myClass
//Your objective c code here....
-(void)hello:(int)num1
{
NSLog(@"Hello!!!!!!");
}
@end

Krok 2

Teraz zaimplementujemy funkcję c ++, aby wywołać funkcję celu c, którą właśnie utworzyliśmy! W tym celu zdefiniujemy plik .mm i odpowiadający mu plik nagłówkowy (plik ".mm" ma być tutaj użyty, ponieważ będziemy mogli używać zarówno kodowania Objective C, jak i C ++ w pliku)

// Plik nagłówkowy (nazywamy go „ObjCCall.h”)

#ifndef __test2__ObjCCall__
#define __test2__ObjCCall__
#include <stdio.h>
class ObjCCall
{
public:
static void objectiveC_Call(); //We define a static method to call the function directly using the class_name
};
#endif /* defined(__test2__ObjCCall__) */

// Odpowiedni plik celu C ++ (nazywamy go „ObjCCall.mm”)

#include "ObjCCall.h"
#include "ObjCFunc.h"
void ObjCCall::objectiveC_Call()
{
//Objective C code calling.....
myClass *obj=[[myClass alloc]init]; //Allocating the new object for the objective C   class we created
[obj hello:(100)];   //Calling the function we defined
}

Krok 3

Wywołanie funkcji C ++ (która faktycznie wywołuje metodę celu c)

#ifndef __HELLOWORLD_SCENE_H__
#define __HELLOWORLD_SCENE_H__
#include "cocos2d.h"
#include "ObjCCall.h"
class HelloWorld : public cocos2d::Layer
{
public:
// there's no 'id' in cpp, so we recommend returning the class instance pointer
static cocos2d::Scene* createScene();
// Here's a difference. Method 'init' in cocos2d-x returns bool, instead of returning  'id' in cocos2d-iphone
virtual bool init();
// a selector callback
void menuCloseCallback(cocos2d::Ref* pSender);
void ObCCall();  //definition
// implement the "static create()" method manually
CREATE_FUNC(HelloWorld);
};
#endif // __HELLOWORLD_SCENE_H__

//Ostateczne wezwanie

#include "HelloWorldScene.h"
#include "ObjCCall.h"
USING_NS_CC;
Scene* HelloWorld::createScene()
{
// 'scene' is an autorelease object
auto scene = Scene::create();
// 'layer' is an autorelease object
auto layer = HelloWorld::create();
// add layer as a child to scene
scene->addChild(layer);
// return the scene
return scene;
}
// on "init" you need to initialize your instance
bool HelloWorld::init()
{
//////////////////////////////
// 1. super init first
if ( !Layer::init() )
{
    return false;
}
Size visibleSize = Director::getInstance()->getVisibleSize();
Vec2 origin = Director::getInstance()->getVisibleOrigin();

/////////////////////////////
// 2. add a menu item with "X" image, which is clicked to quit the program
//    you may modify it.

// add a "close" icon to exit the progress. it's an autorelease object
auto closeItem = MenuItemImage::create(
                                       "CloseNormal.png",
                                       "CloseSelected.png",
                                       CC_CALLBACK_1(HelloWorld::menuCloseCallback,  this));

closeItem->setPosition(Vec2(origin.x + visibleSize.width - closeItem->getContentSize().width/2 ,
                            origin.y + closeItem->getContentSize().height/2));

// create menu, it's an autorelease object
auto menu = Menu::create(closeItem, NULL);
menu->setPosition(Vec2::ZERO);
this->addChild(menu, 1);

/////////////////////////////
// 3. add your codes below...

// add a label shows "Hello World"
// create and initialize a label

auto label = Label::createWithTTF("Hello World", "fonts/Marker Felt.ttf", 24);

// position the label on the center of the screen
label->setPosition(Vec2(origin.x + visibleSize.width/2,
                        origin.y + visibleSize.height - label- >getContentSize().height));
// add the label as a child to this layer
this->addChild(label, 1);
// add "HelloWorld" splash screen"
auto sprite = Sprite::create("HelloWorld.png");
// position the sprite on the center of the screen
sprite->setPosition(Vec2(visibleSize.width/2 + origin.x, visibleSize.height/2 +     origin.y));
// add the sprite as a child to this layer
this->addChild(sprite, 0);
this->ObCCall();   //first call
return true;
}
void HelloWorld::ObCCall()  //Definition
{
ObjCCall::objectiveC_Call();  //Final Call  
}
void HelloWorld::menuCloseCallback(Ref* pSender)
{
#if (CC_TARGET_PLATFORM == CC_PLATFORM_WP8) || (CC_TARGET_PLATFORM ==   CC_PLATFORM_WINRT)
MessageBox("You pressed the close button. Windows Store Apps do not implement a close    button.","Alert");
return;
#endif
Director::getInstance()->end();
#if (CC_TARGET_PLATFORM == CC_PLATFORM_IOS)
exit(0);
#endif
}

Mam nadzieję, że to zadziała!

Mishal Shah
źródło
9

Musisz sprawić, by Twój plik C ++ był traktowany jako Objective-C ++. Możesz to zrobić w xcode, zmieniając nazwę foo.cpp na foo.mm (.mm to rozszerzenie obj-c ++). Wtedy, jak powiedzieli inni, zadziała standardowa składnia komunikatów obj-c.

olliej
źródło
1

Czasami zmiana nazwy .cpp na .mm nie jest dobrym pomysłem, zwłaszcza gdy projekt jest wieloplatformowy. W tym przypadku dla projektu xcode otwieram plik projektu xcode przez TextEdit, znalazłem ciąg, którego zawartość interesuje plik, powinien wyglądać następująco:

/* OnlineManager.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = OnlineManager.cpp; sourceTree = "<group>"; };

a następnie zmień typ pliku z sourcecode.cpp.cpp na sourcecode.cpp.objcpp

/* OnlineManager.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = **sourcecode.cpp.objcpp**; path = OnlineManager.cpp; sourceTree = "<group>"; };

Jest to równoważne zmianie nazwy .cpp na .mm

Yevgeniy Logachev
źródło
1

Możesz również wywołać środowisko uruchomieniowe Objective-C, aby wywołać metodę.

Maxthon Chan
źródło
1

Powyższa odpowiedź @ DawidDrozd jest doskonała.

Dodałbym jeden punkt. Najnowsze wersje kompilatora Clang narzekają na wymaganie „rzutowania pomostowego” przy próbie użycia jego kodu.

Wydaje się to rozsądne: użycie trampoliny stwarza potencjalny błąd: ponieważ klasy Objective-C są liczone jako referencje, jeśli przekażemy ich adres jako void *, ryzykujemy zawieszenie wskaźnika, jeśli klasa jest bezużyteczna, podczas gdy wywołanie zwrotne jest nadal aktywny.

Rozwiązanie 1) Kakao zapewnia funkcje makr CFBridgingRetain i CFBridgingRelease, które przypuszczalnie dodają i odejmują jedną z liczby referencji obiektu Objective-C. Dlatego powinniśmy być ostrożni w przypadku wielu wywołań zwrotnych, aby zwolnić taką samą liczbę razy, jaką zachowujemy.

// C++ Module
#include <functional>

void cppFnRequiringCallback(std::function<void(void)> callback) {
        callback();
}

//Objective-C Module
#import "CppFnRequiringCallback.h"

@interface MyObj : NSObject
- (void) callCppFunction;
- (void) myCallbackFn;
@end

void cppTrampoline(const void *caller) {
        id callerObjC = CFBridgingRelease(caller);
        [callerObjC myCallbackFn];
}

@implementation MyObj
- (void) callCppFunction {
        auto callback = [self]() {
                const void *caller = CFBridgingRetain(self);
                cppTrampoline(caller);
        };
        cppFnRequiringCallback(callback);
}

- (void) myCallbackFn {
    NSLog(@"Received callback.");
}
@end

Rozwiązanie 2) Alternatywą jest użycie odpowiednika słabego odniesienia (tj. Brak zmiany liczby zatrzymań) bez dodatkowego bezpieczeństwa.

Język Objective-C zapewnia kwalifikator rzutowania __bridge, aby to zrobić (CFBridgingRetain i CFBridgingRelease wydają się być cienkimi opakowaniami Cocoa na konstrukcjach języka Objective-C __bridge_retained i release, ale Cocoa nie wydaje się mieć odpowiednika dla __bridge).

Wymagane zmiany to:

void cppTrampoline(void *caller) {
        id callerObjC = (__bridge id)caller;
        [callerObjC myCallbackFn];
}

- (void) callCppFunction {
        auto callback = [self]() {
                void *caller = (__bridge void *)self;
                cppTrampoline(caller);
        };
        cppFunctionRequiringCallback(callback);
}
QuesterZen
źródło
Muszę przyznać, że jestem nieco podejrzliwy co do tego, czy Rozwiązanie 1 zapewnia jakiekolwiek dodatkowe bezpieczeństwo pomimo wszystkich zachowań / zwolnień. Jedną kwestią jest to, że przekazujemy kopię selfdo zamknięcia, które może stać się nieaktualne. Inną kwestią jest to, że nie jest jasne, jak współdziała to z automatycznym zliczaniem odwołań i czy kompilator może dowiedzieć się, co się dzieje. W praktyce nie udało mi się stworzyć sytuacji, w której którakolwiek z wersji zawodzi w prostym jednomodułowym przykładzie zabawki.
QuesterZen
-1

Możesz mieszać C ++ z Objectiv-C (Objective C ++). Napisz metodę C ++ w swojej klasie Objective C ++, która po prostu wywołuje [context renderbufferStorage:GL_RENDERBUFFER fromDrawable:(CAEAGLLayer*)self.layer];i wywołuje ją z Twojego C ++.

Nie próbowałem tego wcześniej, ale daj mu szansę i podziel się z nami wynikami.

hhafez
źródło
2
Ale problem polega na tym, że jak mogę to nazwać ... ponieważ jeśli dołączę „EAGLview.h” do mojej klasy C ++, otrzymuję tysiące błędów.
juvenis