Usando C/C++ no iOS

Quando fui desenvolver uma aplicação móvel baseada em OpenGL ES, de maneira que fosse de fácil conversão para vários sistemas, as opções de linguagens caíram bastante. No caso do iOS, utilizar Objective-C poderia ser a melhor opção, mas daí estaria preso ao sistema da Apple. Ainda assim, grande parte dos APIs do sistema são voltados para o uso com Objective-C, então não dá para utilizar exclusivamente C++, que é a opção mais viável quando se pensa em compilar o mesmo programa para os vários sistemas disponíveis.

Felizmente é muito fácil fazer essa comunicação entre as 2 linguagens no iOS.

Projeto para iOS

Com um projeto em branco no XCode, e colocando um GLKit View no controlador inicial é o suficiente para termos o necessário para nosso código OpenGL poder desenhar na tela do dispositivo.

O que precisamos fazer é chamar nosso código C++ nos vários eventos que devem ser implementados do lado Objective-C. Como o ele é capaz de chamar código C diretamente, é só aproveitar disso e criar um arquivo com as declarações das funções que vão ser usadas pelo Objective-C para se comunicar com o código C++.

No meu caso, precisava cuidar dos seguintes eventos:

Para tratar estes eventos, criei o seguinte cabeçalho C:

#pragma once

#include <stddef.h>

#ifdef __cplusplus
extern "C" {
#endif

#define TOUCH_START  0
#define TOUCH_MOVE   1
#define TOUCH_END    2
#define TOUCH_CANCEL 3
// didFinishLauchWithOptions
void GAME_startup(void);
// viewDidLoad
void GAME_init(int width, int height, float scale, bool tablet);
// applicationWillResignActive
void GAME_lostFocus(void);
// applicationDidBecomeActive
void GAME_restoredFocus(void);
// applicationDidEnterBackground 
void GAME_saveState(void); 
// drawInRect
void GAME_update(void); 
// Touch
void GAME_processTouch(size_t id, unsigned action, unsigned x, unsigned y);

#ifdef __cplusplus
}
#endif

Lado do Objective-C

No lado do Objective-C, não poderia ser mais fácil: é só incluir nosso arquivo de cabeçalho e chamar as funções corretas.

AppDelegate

No arquivo que implementa nosso AppDelegate, não precisamos de quase nada além de chamar os métodos. Devo lembrar que neste momento, não temos necessariamente um contexto OpenGL, então devemos nos limitar a utilizar apenas as coisas que não dependem do sistema.

#import "AppDelegate.h"
#import "header_cpp.h"

@interface IOS_AppDelegate ()

@end

@implementation IOS_AppDelegate


- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    GAME_startup();
    return YES;
}

- (void)applicationWillResignActive:(UIApplication *)application {
    GAME_lostFocus();
}

- (void)applicationDidEnterBackground:(UIApplication *)application {
    GAME_saveState();
}

- (void)applicationWillEnterForeground:(UIApplication *)application {
}

- (void)applicationDidBecomeActive:(UIApplication *)application {
    GAME_restoredFocus();
}

- (void)applicationWillTerminate:(UIApplication *)application {
}

@end

ViewController

Já nosso ViewController.m deve fazer algumas coisas antes de passar o controle para o C++, como tratar os eventos de maneira adequada, criar e ajustar o contexto OpenGL antes de qualquer código OpenGL, etc.

#import "ViewController.h"
#import "header_cpp.h"

@interface IOS_ViewController () {}
@end

@implementation IOS_ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // setup OpenGL ES 2.0
    EAGLContext* ctx = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
    if (!ctx) {
        NSLog(@"Context OpenGL error.");
        exit(1);
    }
    GLKView* view = (GLKView*) self.view;
    view.context = ctx;
    [EAGLContext setCurrentContext: ctx];

    // viewport setup
    int h = view.bounds.size.height;
    int w = view.bounds.size.width;
    float s = view.contentScaleFactor;
    bool tablet = UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad;
    GAME_init(w, h, s, tablet);
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

#pragma MARK -- GLKView

- (void)update
{
}

- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect
{
    if ( [EAGLContext currentContext] != view.context ) {
        [EAGLContext setCurrentContext: view.context];
    }
    GAME_update();
}

#pragma MARK -- touch

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    [super touchesBegan:touches withEvent:event];
    GLKView* view = (GLKView*) self.view;
    for (UITouch * t in touches) {
        CGPoint p = [t locationInView:self.view];
        GAME_processTouch((size_t)t, TOUCH_START, p.x, view.bounds.size.height - p.y);
    }
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    [super touchesMoved:touches withEvent:event];
    GLKView* view = (GLKView*) self.view;
    for (UITouch * t in touches) {
        CGPoint p = [t locationInView:self.view];
        GAME_processTouch((size_t)t, TOUCH_MOVE, p.x, view.bounds.size.height - p.y);
    }
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    [super touchesEnded:touches withEvent:event];
    GLKView* view = (GLKView*) self.view;
    for (UITouch * t in touches) {
        CGPoint p = [t locationInView:self.view];
        GAME_processTouch((size_t)t, TOUCH_END, p.x, view.bounds.size.height - p.y);
    }
}

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
    [super touchesCancelled:touches withEvent:event];
    GLKView* view = (GLKView*) self.view;
    for (UITouch * t in touches) {
        CGPoint p = [t locationInView:self.view];
        GAME_processTouch((size_t)t, TOUCH_CANCEL, p.x, view.bounds.size.height - p.y);
    }
}
@end

Lado C++

Agora, é só criar um arquivo .cpp que inclua e defina as funções declaradas no nosso cabeçalho, e pronto!

Outra coisa que tive que fazer foi uma maneira de como ter o caminho correto para os arquivos dentro de meu aplicativo, para ler imagens, texturas e outras coisas. Utilizando as funções mais básicas do sistema iOS, disponíveis para C via CoreFoundation, dá para fazer uma função auxiliar para ser utilizada em conjunto com qualquer função C++, inclusive as funções de iostream. Segue o código para quem interessar.

Depois de getFilePath ser declarado em algum arquivo de cabeçalho... é só definir:

#include <string>
#include <CoreFoundation/CoreFoundation.h>

#define BUF_SIZE 1024

std::string getFilePath(const std::string &filename) {
    char buffer[BUF_SIZE];
    auto bundle = CFBundleGetMainBundle();
    auto str = CFStringCreateWithCString(NULL, filename.c_str(), kCFStringEncodingUTF8);
    auto url = CFBundleCopyResourceURL(bundle,str, NULL, NULL);
    CFRelease(str);
    if (url == NULL) return {""};
    CFURLGetFileSystemRepresentation(url, true, (UInt8*)&buffer, BUF_SIZE);
    CFRelease(url);
    return {buffer};
}

Espero que tenha ajudado quem está começando a utilizar C++ em iOS!