An Introduction to ShiVa iOS plugins

ShiVa games run natively on a large number of platforms without the need for the developer to adjust anything. Our STK + engine architecture was designed with the "build once, run everywhere" philosophy in mind. There are times however when you want to use a feature that is native to your target platform, in which case you have to write your own plugins. If the target platform native API is coded largely in C or C++, this requires no effort. As soon as you encounter a different language like Objective-C in the Apple platforms, it becomes a much more difficult story. This tutorial will give you an introduction on how to code plugins in the Apple ecosystem and write bridge code between ShiVa and Objective-C/C++.

Method 1: Sticking to C++

The easiest way of writing ShiVa plugins for iOS is sticking to C++ and avoiding any platform-native code entirely. If the package you want to integrate in your plugin is written in C or C++, this is the best choice.

You can find the iOS plugin project in the plugin "Make" directory:

As always, you can blindly accept the suggestion Xcode throws at you to bring the project settings up to the version standards in your Xcode environment:

If you are writing to a certain C++ language specification, make sure to select the corresponding language dialect in the "Build Settings":

Note how all C++ code files do have the ".cpp" extension, supplemented by their ".h" headers.

Method 2: Calling Objective-C from C++

Writing purely in C++ will not allow you interface with native iOS calls. In order to use Objective-C and its syntax in your C++ code, you have to tell the compiler to treat your C++ code as Objective-C++, which is a superset of C++ - like Objective-C is to C. In other words, you can write C++ code in Objective-C++ files, but the C++ compiler will not understand your Objective-C++ commands.

Converting a C++ file to Objective-C++ is easy. Changing the ".cpp" file extension to ".mm" tells Xcode to compile the file with the Objective-C++ compiler. To make the process fast and easy, go to "File> New> File..." and select one of the following options. None of the choices fit our purpose perfectly, so whatever you select, you will have to make changes:

- "Objective-C File": creates an empty .h and .m file pair, which you have to rename to .h and .mm
- "C++ File": creates an empty .hpp and .cpp file pair, which you have to rename to .h and .mm
- "Cocoa Touch class": creates a skeleton .h and .m interface/implementation file pair, which you have to clear out and rename to .h and .mm

Inside the .mm file, you can now mix both C++ and Objective-C calls freely, like so:

  1. void logVersion () {
  2.  
  3. NSString * version = [[NSBundle mainBundle] objectForInfoDictionaryKey: @"CFBundleShortVersionString"];
  4. S3DX::log.message("Running running on version ",
  5. [version cStringUsingEncoding:NSUTF8StringEncoding],
  6. " build ",
  7. [build cStringUsingEncoding:NSUTF8StringEncoding] );
  8.  
  9. return;
  10. }

This compiles and works as expected:

Common Pitfalls

While mixing ShiVa C++ and Objective-C++ code, you must be aware of a number of potential pitfalls.

Similar names, different languages

Even though C++ and Objective-C++ have similar names, they are two different languages which will be compiled independently from each other. You will need to redefine all the C++ imports you need in your .mm file in order to use them. In the case of ShiVa plugins, you will need to

  1. #include "Plugin.h"

in order to use any S3DX:: calls.

Headers and Includes

C++ references files through the #include keyword, Objective-C uses #import. If you wish to import and Objective-C header into a C++ file, you need to encapsulate the include into a preprocessor macro:

  1. #include "MyCppClass.h"
  2. #include <vector>
  3.  
  4. #ifdef __OBJC__
  5. #import "MyObjCCode.h"
  6. #endif

Nil

Objective-C brings its own set of standard types. One of them in particular, NIL, conflicts with the nil type in ShiVa. Either cleanly separate ShiVa C++ code from your .mm code by not including Plugin.h in object code that depends on NIL(recommended), or replace all NILs with NULLs (lazy last resort).

Auto

Objective-C types can often not be correctly deduced by the C++11(+) "auto" keyword. Prefer declaring your Objective-C types explicitly.

Class members

It is technically possible to use Objective-C typed members in your C++ class. As with #imports, you have to encapsulate those in an __OBJC__ macro:

  1. class CCM {
  2. private:
  3. const char * _sEventMotion = "onCMMotion";
  4.  
  5. #ifdef __OBJC__
  6. NSTimeInterval _objcCMPollingRate = 1.0/60.0;
  7. CMMotionManager * _objcCMM;
  8. NSOperationQueue * _objcCMQueue;
  9. #endif
  10.  
  11. public:
  12. CCM();
  13. ~CCM();
  14. void engineEventLoop();
  15.  
  16. };

Otherwise you will get unknown type errors:

Method 3: Bridge Classes

By far the most common scenario you will face is this: You found a 3rd party SDK for iOS, or want to integrate new iOS native core functionality into ShiVa, but those are written in Objective-C and make use of Interfaces, Implementations, Delegates, Protocols and all the other things that cannot be used directly in C++. To make these SDKs available in ShiVa, you have to design a bridge class.

A bridge class consists of 2 halves, a C++ side and an Objective-C++ side, both in .mm files. We can create the missing .h/.mm pair like before by creating a Cocoa Touch Class from the New File dialogue, however this time our entries matter, since we will actually use the skeleton code produced by Xcode. Choose a name for your bridge class (must be different from the C++ class), and inherit fron NSObject unless you have good reasons to do something else.

This will leave us with 4 files (2x .h, 2x .mm). In this example, "BridgeClass.*" is the mostly-C++ class, and "BridgeClass_o.*" is the mostly-Objective-C++ class.

After applying the include/member variable rules from above, all we need is a way for these two classes to interact. One convenient way to do this is via class pointers. The C++ header file carries the "(id)" of the Objective-C class instance as a member variable:

  1. //
  2. // C++ header
  3. // BridgeClass.h
  4.  
  5. #pragma once
  6.  
  7. class BridgeClass {
  8. private:
  9. // target AI name
  10. const char * _sAI = "game";
  11.  
  12. // pointer to the bridge ObjC-Class
  13. #ifdef __OBJC__
  14. id _objcClass;
  15. #endif
  16.  
  17. public:
  18. // constructor/destructor
  19. BridgeClass();
  20. ~BridgeClass();
  21.  
  22. // user plugin functions
  23. bool init (const char * sAI);
  24. };

Likewise, the Objective-C class lists the BridgeClass pointer as one of its properties:

  1. //
  2. // Objective-C++ header
  3. // BridgeClass_o.h
  4.  
  5. #pragma once
  6.  
  7. // C++ ShiVa plugin include
  8. #include "Plugin.h"
  9.  
  10. // include C++ Bridge class header
  11. #include "BridgeClass.h"
  12.  
  13. // Objective-C Class
  14.  
  15. @interface BridgeClass_o : NSObject
  16.  
  17. @property BridgeClass * pointerToCpp;
  18.  
  19. @end

As soon as the C++ class is constructed, it allocates momory for the Objective-C class counterpart and stores its (id) inside the member variable. Directly after that, the class pointer "this" is sent to Objective-C class through the automatically synthesized function "setPointerToCpp" which stores the result in the "pointerToCpp" @property. Since we are not using Apple ARC, we must make sure to destroy every Objective-C object ourselves by releasing it at the appropriate time:

  1. //
  2. // C++ class implementation
  3. // BridgeClass.mm
  4.  
  5. #include "BridgeClass.h"
  6. #import "BridgeClass_o.h"
  7.  
  8. // constructor/destructor
  9. BridgeClass::BridgeClass() {
  10. _objcClass = NULL;
  11. _objcClass = [[BridgeClass_o alloc] init];
  12. [_objcClass setPointerToCpp:this];
  13. }
  14.  
  15. BridgeClass::~BridgeClass() {
  16. [_objcClass release];
  17. }

The init function inside the Objective-C class implementation is as simple as it can get. Note how the keyword "this" is changed to "self" in Objective-C, but otherwise does practically the same:

  1. //
  2. // Objective-C Implementation
  3. // BridgeClass_o.mm
  4.  
  5. #import "BridgeClass_o.h"
  6.  
  7. @implementation BridgeClass_o
  8.  
  9. - (id)init
  10. {
  11. self = [super init];
  12. if (self) {
  13. //initialize something here if needed
  14. }
  15. return self;
  16. }
  17.  
  18. @end

With a connection like this established, you can now call C++ class member functions from Objective-C and vice versa. For instance, here is how you would invoke the "BridgeClass::logMessageFromObjC" method from the Objective-C function "createAlert":

  1. // Objective-C
  2. - (BOOL)createAlert {
  3. if (!_pointerToCpp)
  4. return false;
  5.  
  6. // auto-syntehsized underscore prefix!
  7. _pointerToCpp->logMessageFromObjC("This is a log from ObjC -> C++");
  8. return true;
  9. }

The other direction makes use of the [] syntax as we have seen in method 2. For instance, here is how you would invoke the "createAlertWithString" method from the Objective-C class behind (id)_objcClass and transmit a single string argument:

  1. bool BridgeClass::createAlert (const char * _sMessage){
  2. if (_objcClass == nullptr)
  3. return false;
  4.  
  5. [_objcClass createAlertWithString:_sMessage];
  6. return true;
  7. };

Example: Using a native iOS alert popup

As a practical example, I want to show how to use a native iOS framework (UIKit) and pop up a standard alert on the screen with a couple of buttons. First, we need to include the header of the framework we want to use:

  1. // BridgeClass_o.h Objective-C++ header
  2. // for alert box
  3. #import <UIKit/UIKit.h>

Next, we need to add a class method which allows ShiVa to call into our Objective-C class:

  1. // BridgeClass_o.h Objective-C++ header
  2. - (BOOL)createAlertWithString: (const char *) fromShiVa;

The implementation looks as follows:

  1. // BridgeClass_o.mm Objective-C++ implementation
  2. - (BOOL)createAlertWithString: (const char *) fromShiVa {
  3.  
  4. S3DX::log.message("ObjC: message received!");
  5. S3DX::log.message(fromShiVa);
  6.  
  7. UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"ShiVa Alert"
  8. message:[NSString stringWithUTF8String:fromShiVa]
  9. delegate:self
  10. cancelButtonTitle:@"Cancel"
  11. otherButtonTitles:@"Choice", @"Another one", NULL];
  12. [alert show];
  13. // Not using ARC? Release alert view
  14. [alert release];
  15. return true;
  16. }

UIAlertView is long deprecated, but it still works and allows me to easily show another useful Objective-C feature you will need to deal with in your plugins: delegates.

Delegates

Delegates can be thought of as quite similar to ShiVa handlers. When you send a message (user.sendMessage()/object.sendMessage()), you expect your target AI to have an event handler with a certain number of arguments which processes your message. Similarly, UIAlertView sends messages to a delegate with certain predefined functions using a protocol.

In the code above, we defined the delegate to be "self", which means we have to implement the delegate functions (our "event handlers") in the BridgeClass_o.mm file:

  1. - (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
  2. S3DX::log.message("Alert button #", (float)buttonIndex, "pressed!");
  3. }

"alertView" and its prototype are predefined by UIKit and will get called automatically when a button belonging to (UIAlertView *) is clicked.

It is customary to declare the delegate protocol on the interface using angled brackets:

  1. @interface BridgeClass_o : NSObject <UIAlertViewDelegate>
  2. ...
  3. @end

The result of the code looks like this:

All log messages are visible in the Xcode debugger: