I just made the first release of my voxel editor project goxel for iOS, and I wanted to write a bit about the process.
It’s not the first time I work on a mobile port of C/C++ application, so the process was quite easy. The only new challenge this time is that I used swift on the platform code (instead of objective-C).
The global structure of the code consists of a core written in C (or C++), and a small platform dependant layer on top of it to create the application context and handle the inputs.
An ugly schematisation of what is going on:
+-----------+ +----------+
| platform | | core (C) |
| (swift) | | |
+-----------+ +----------+
| |
+ create context |
| |
+--->+ loop |
| | |
| +-get inputs |
| | |
| +------------------->+-core_iter(inputs)
| |
|native +<----------------------+
| call | .
| | .
| +---------------------->+
| |
| |
| <-----------------------+
| |
+-------+ End loop
The important points to keep in mind are:
- Make sure the C code compiles with clang on iOS.
- Separate the inputs listening from the core.
- Have a mechanism to call platform dependant functions from the C code.
- Be careful with the OpenGL version used.
Compiling the C code
Compiling the C code to iOS was totally straightforward: I just added the files to a new xcode project, and all of them compiled directly.
The only issue was that I got many warnings related to numerical type conversions: I tend to omit casting when I convert a number to a lower precision type. On linux, gcc doesn’t seem to care, but clang on xcode does. Since I was not going to change my code for this I just added a rule to ignore those warnings in xcode build settings.
The reason I didn’t have any trouble was because I am very careful about the dependencies I use in my code. In the case of goxel, the only external libraries used are:
- dear imgui for the UI.
- stb image for png loading and writing.
- uthash for list and hash data structures.
- inih for parsing ini files.
All those libraries are very small, can be directly compiled as part of a project code, and have no dependencies other than the standard C library.
Also, by not using C++, but only C99, I ensured that no part of my code was using language features not implemented properly by all compilers.
A nice feature of xcode is that it gives you by default a header file (PrefixHeader.pch) that is included before any compiled file. It’s the prefect place to put some global macro declarations to customize the application on iOS.
Inputs and OpenGL context creation.
The biggest problem when porting an application to a new platform is how to handle the inputs (keyboard, mouse position and state, etc). On the desktop version, goxel relies on glfw, but on iOS this is not available.
To make it easy, the best way it to clearly separate the inputs capture
from the core. In the case of goxel, all the inputs are passed to the core
using a single data structure instance (struct inputs
):
typedef struct {
vec2_t pos;
bool down[3];
} touch_t;
typedef struct inputs
{
int window_size[2];
float scale;
bool keys[512]; // Table of all the pressed keys.
uint32_t chars[16];
touch_t touches[4];
float mouse_wheel;
int framebuffer; // Screen framebuffer
} inputs_t;
The platform specific code is in charge of updating this structure at each iteration and pass it to the core iter function. The structure is also used to pass the OpenGL context information to the core: screen size, framebuffer index, and scale (for retina display).
C and swift communication
The inputs_t
structure is unfortunately not totally enough. Sometime
we also need to be able to call platform specific code from the C code.
For example, when the user double tap on an input widget, we want to open the iOS virtual keyboard. So we need to somehow call a swift function from the C code.
It’s not really possible to expose a swift function to C, but it’s possible to convert to swift lambda to a C function pointer, and then have the C code call it.
So in order to expose platform dependant function to C, all we need to do is to maintain a list of callback functions in the C code, and initialize them with swift lambdas. The only trick is that if we want to use class methods, we also need to pass a pointer to the object instance as an argument to the callback.
In the C code:
struct {
/* ... */
// Used to communicate with swift.
struct {
void *user;
void (*show_keyboard)(bool hasText, void *user);
/* ... Other callbacks ... */
} callbacks;
} goxel_t;
In the swift code:
self.goxel.callbacks.user = Unmanaged.passUnretained(self).toOpaque()
self.goxel.callbacks.show_keyboard = { (hasText, user) in
let this = Unmanaged<ViewController>
.fromOpaque(user!)
.takeUnretainedValue()
/* ... Rest of the function */
}
Now we can call the function from anywhere in the C code:
goxel->callbacks.show_keyboard(true, goxel->callbacks.user);
OpenGL
In order to make goxel multi platform, I decided to restrict myself to OpenGL ES2. It has some annoying limitations, but ES2 is implemented on pretty much all possible platforms (including javascript).
I still had to profile my shaders to make sure that they would not be too slow on a phone. It turned out that the shadow map was actually slowing down the rendering, so I disabled it for the moment in the mobile version.
Conclusion
Writing cross platform application is still an unsolved problem. Currently one common approach is to to use html 5 for its good widget support, this is however still too slow for resource heavy applications. If having a simplistic user interface is acceptable, writing the application in C is in my opinion a valid option.