Tutorial: Quick and easy C++ native code in iOS with FFI
Posted: Thu Sep 22, 2016 12:13 am
Obs.: I'm sorry if this is not the right place for this kind of topic. I really don't know where to post something like this.
The purpose of this tutorial is to show an easy and quick way of running C/C++ code from Lua (inside Love), in iOS, without really modifying Love source code or using Lua API. The problem is: FFI is supposed to be used with dynamic libraries (which aren't allowed in iOS).
I made this tutorial because figuring out whether it was possible to use FFI the way I do here (dirty and quick) in iOS was a bit time demanding, even though it is possible and very easy to set up. I suppose this topic can be useful for someone, in the future.
Before anything else, I'll apologize for the lack of references. If anything seems too odd and reference demanding, just ask.
Running C/C++ code in iOS has the following advantages:
And before any criticism regarding the uglyness of the present solution (mainly due to the third caveat), it can, at least, be used for quick prototyping and benchmarking of native code for a game already on the rails. And I agree with those criticisms. It is an ugly solution.
So, finally, let's get our hands dirty, shall we?
Programming C++ code is out of the scope of this tutorial. We will keep things simple. In this tutorial, we are going to interface the priority queue from C++ standard library to Lua.
First step: download and open the xcode project of Love for iOS. Information regarding this step can be obtained at the wiki.
After the project is open, we'll create our CPP and HPP files. We'll put them in a separate group from the rest of Love, so that we don't mix things. The following pictures illustrate how to do that.
Before we start coding, let's change the building options so that we can call our functions later, from lua. Click on the project's name, in the list on the left side of the screen. Select the Building Settings tab. Click on "all", on the top, to show all options.
You can use the search bar to help you out with the next part. We want to change the following options:
Basically, we did these changes for two reasons:
Then, on the list on left, select Run, and on the right pane, change Build Configuration to Release or Distribution (as you wish).
Now we are ready to code our example. The idea here is to interface std::priority_queue<std::pair<double, int> >. That is, we want a priority queue that, given a number of integer elements, each one associated with a real priority, pops the elements so that the element with greatest priority comes out first.
Open the header file (MyCode.hpp, in my case).
After the include line, add the following:
Note that we don't really need the My_PQ_Wrapper struct. We could directly use void*. However, if we did that, we should have to trick FFI later, because it won't set metatypes to void*. So, in order to keep things simple, we are wrapping void* here. What each function must do is easily guessable from the name. The extern "C" is a hint that we want our symbols to stay visible externally.
Open the cpp file. Its final content is the following:
There's no secret to it. Any questions, just ask.
We can jump to Lua, now. Minimize xcode and create a new main.lua as you would if you were going to create a new game.
First, we need to let FFI knows about our types and functions. We do that with ffi.cdef:
As our code is statically linked to luajit (they are in the same binary file), we don't need to do anything else before being able to call our C functions. However, for the sake of awareness, note note that you would have to open the dynamic library before using it, in other operating systems.
Now, PQLib is the namespace of our functions (it is a table that contains them). When JIT is on, Mike Pall doesn't recommend creating variables to hold C functions directly (instead, use Namespace.Function(...)). He says that the JIT compiler performs better this way. Even though JIT is off, we will keep the recommended practice here (because the code will probably be multiplatform).
To make things easier to use, we'll create a metatable for our C:
We'll need a destructor so that the memory is empty after the priority queue is used:
And a function that creates a priority queue and associate its destructor to the new instance:
We can finally test it!
Run Love through xcode (so that you can read the output in the console). To open a .love file on my iPad, I usually run a http server on my computer and download the file on the iPad. You can also use iTunes to transfer the game.
The results on the console:
The purpose of this tutorial is to show an easy and quick way of running C/C++ code from Lua (inside Love), in iOS, without really modifying Love source code or using Lua API. The problem is: FFI is supposed to be used with dynamic libraries (which aren't allowed in iOS).
I made this tutorial because figuring out whether it was possible to use FFI the way I do here (dirty and quick) in iOS was a bit time demanding, even though it is possible and very easy to set up. I suppose this topic can be useful for someone, in the future.
Before anything else, I'll apologize for the lack of references. If anything seems too odd and reference demanding, just ask.
Running C/C++ code in iOS has the following advantages:
- As Luajit is not jitted on iOS (only interpreted), C/C++ code usually is much faster and requires less energy;
- You can use and/or interface native components;
- Using FFI avoids modifying Löve source code and is, in general, easier.
- When Lua is jitted, calling C functions with FFI is usually as fast as a C -> C call, therefore faster than calling through LUA API (used by Löve). However, calling C functions with FFI has a considerable overhead when the JIT compiler is off and is slower than LUA API. I read a thread in the Luajit mailing list where Luajit's main developer (Mike Pall) warned that calling C functions repeatedly (as in a loop) will hurt the performance (badly) when JIT is off;
- No callback functions to FFI calls! When JIT is on, you can pass Lua functions as arguments to C functions. In our case, this will not be possible.
- In order to prevent our C/C++ code from being stripped from the binary, we'll have to disable linking optimisations. This will mainly impact the binary size. In my experience, the app became just slightly larger.
And before any criticism regarding the uglyness of the present solution (mainly due to the third caveat), it can, at least, be used for quick prototyping and benchmarking of native code for a game already on the rails. And I agree with those criticisms. It is an ugly solution.
So, finally, let's get our hands dirty, shall we?
Programming C++ code is out of the scope of this tutorial. We will keep things simple. In this tutorial, we are going to interface the priority queue from C++ standard library to Lua.
First step: download and open the xcode project of Love for iOS. Information regarding this step can be obtained at the wiki.
After the project is open, we'll create our CPP and HPP files. We'll put them in a separate group from the rest of Love, so that we don't mix things. The following pictures illustrate how to do that.
Before we start coding, let's change the building options so that we can call our functions later, from lua. Click on the project's name, in the list on the left side of the screen. Select the Building Settings tab. Click on "all", on the top, to show all options.
You can use the search bar to help you out with the next part. We want to change the following options:
- Strip Style -> Debugging Symbols
- Dead Code Stripping -> No
- Link-Time Optimisations -> No (note that this option is No for debug. Change it for release and distribution as well)
- Symbols Hidden by Default -> No
Basically, we did these changes for two reasons:
- The list of exported symbols is kinda an Yellow Pages. FFI searches it to find where our functions are implemented. Thus, we need to keep symbols for our functions there;
- Functions that are never called are typically removed from the final binary, because they take space for nothing. As FFI decides which functions must be called at runtime, the compiler cannot foresee that our functions will be needed, and will remove them (as they are never called inside Love). This is why we have to disable dead code stripping and linking optimisations.
Then, on the list on left, select Run, and on the right pane, change Build Configuration to Release or Distribution (as you wish).
Now we are ready to code our example. The idea here is to interface std::priority_queue<std::pair<double, int> >. That is, we want a priority queue that, given a number of integer elements, each one associated with a real priority, pops the elements so that the element with greatest priority comes out first.
Open the header file (MyCode.hpp, in my case).
After the include line, add the following:
Code: Select all
extern "C" {
typedef struct {
void * pointer;
} My_PQ_Wrapper;
My_PQ_Wrapper my_pq_create_new();
void my_pq_delete(My_PQ_Wrapper wrapper);
void my_pq_push_element(My_PQ_Wrapper wrapper, double priority, int element);
int my_pq_pop_element(My_PQ_Wrapper wrapper);
}
Open the cpp file. Its final content is the following:
Code: Select all
#include "MyCode.hpp"
#include <queue>
#include <utility>
My_PQ_Wrapper my_pq_create_new() {
My_PQ_Wrapper wrapper;
auto pq = new std::priority_queue<std::pair<double, int>>();
wrapper.pointer = reinterpret_cast<void*>(pq);
return wrapper;
}
void my_pq_delete(My_PQ_Wrapper wrapper) {
auto pq = reinterpret_cast<std::priority_queue<std::pair<double,int>>*>(wrapper.pointer);
delete pq;
}
void my_pq_push_element(My_PQ_Wrapper wrapper, double priority, int element) {
auto pq = reinterpret_cast<std::priority_queue<std::pair<double,int>>*>(wrapper.pointer);
pq->push(std::make_pair(priority, element));
}
int my_pq_pop_element(My_PQ_Wrapper wrapper) {
auto pq = reinterpret_cast<std::priority_queue<std::pair<double,int>>*>(wrapper.pointer);
auto top = pq->top();
pq->pop();
return top.second;
}
We can jump to Lua, now. Minimize xcode and create a new main.lua as you would if you were going to create a new game.
First, we need to let FFI knows about our types and functions. We do that with ffi.cdef:
Code: Select all
local ffi = require 'ffi'
ffi.cdef [[
typedef struct {
void * pointer;
} My_PQ_Wrapper;
My_PQ_Wrapper my_pq_create_new();
void my_pq_delete(My_PQ_Wrapper wrapper);
void my_pq_push_element(My_PQ_Wrapper wrapper, double priority, int element);
int my_pq_pop_element(My_PQ_Wrapper wrapper);
]]
Code: Select all
local PQLib
if love.system.getOS() == "iOS" then
PQLib = ffi.C
else
PQLib = ffi.load 'dll_location_if_windows_etc'
end
To make things easier to use, we'll create a metatable for our C:
Code: Select all
local mt = {
__index = {
Push = function(self, priority, element)
PQLib.my_pq_push_element(self, priority, element)
end,
Pop = function(self)
return PQLib.my_pq_pop_element(self)
end,
},
}
ffi.metatype("My_PQ_Wrapper", mt)
Code: Select all
local function destructor(pq)
PQLib.my_pq_delete(pq)
end
Code: Select all
local function PriorityQueue()
return ffi.gc(PQLib.my_pq_create_new(), destructor)
end
Code: Select all
local pq = PriorityQueue()
pq:Push(5, 1000)
pq:Push(2, 1001)
pq:Push(10, 1002)
for i = 1, 3 do
print(pq:Pop())
end
The results on the console: