Cross Platform Mobile and Web Development with C++ Explained.
Part 1: Project Setup
Code
The code is in the part1_setup branch of the repository.
Get the tools
Of course we assume you have the tools downloaded and installed, you need XCode on a Mac (I use Appcode but it is not free), Android Studio with the Android NDK and Emscripten. Perhaps also an IDE for C++ like CLion or QtCreator (they both support CMake projects).
I wrote this tutorial on Linux and MacOS, so the file paths and separators are UNIX-like, I do not know if it works on Windows, any feedback is welcome.
Project Structure
.
├── android
│ ├── app
│ │ ├── build.gradle
│ │ ├── proguard-rules.pro
│ │ └── src
│ │ └── main
│ │ ├── AndroidManifest.xml
│ │ ├── cpp
│ │ │ ├── CMakeLists.txt
│ │ │ ├── common_src -> ../../../../../common/src
│ │ │ ├── deps -> ../../../../../deps
│ │ │ └── native-lib.cpp
│ │ ├── java
│ │ │ └── org
│ │ │ └── example
│ │ │ └── xptuto
│ │ │ └── MainActivity.java
│ │ └── res
│ │ ├── drawable
│ │ │ └── ic_launcher_background.xml
│ │ ├── drawable-v24
│ │ │ └── ic_launcher_foreground.xml
│ │ ├── layout
│ │ │ └── activity_main.xml
│ │ ├── mipmap-anydpi-v26
│ │ │ ├── ic_launcher_round.xml
│ │ │ └── ic_launcher.xml
│ │ ├── mipmap-hdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ...
│ │ └── values
│ │ ├── colors.xml
│ │ ├── strings.xml
│ │ └── styles.xml
│ ├── build.gradle
│ ├── gradle
│ │ └── wrapper
│ │ ├── gradle-wrapper.jar
│ │ └── gradle-wrapper.properties
│ ├── gradle.properties
│ ├── gradlew
│ ├── gradlew.bat
│ └── settings.gradle
├── CMakeLists.txt
├── common
│ └── src
│ └── CMakeLists.txt
├── deps
│ ├── CMakeLists.txt
│ └── djinni
│ ├── CMakeLists.txt
│ ├── LICENSE
│ ├── README.md
│ ├── src
│ │ ...
│ │ └── support
│ │ ├── sbt
│ │ ├── sbt-launch.jar
│ │ ├── sbt.resolvers.properties
│ │ └── sbt.security.policy
│ └── support-lib
│ ├── djinni_common.hpp
│ ├── jni
│ │ ├── djinni_main.cpp
│ │ ├── djinni_support.cpp
│ │ ├── djinni_support.hpp
│ │ └── Marshal.hpp
│ ├── proxy_cache_impl.hpp
│ └── proxy_cache_interface.hpp
├── ios
│ ├── xptuto
│ │ ├── AppDelegate.h
│ │ ├── AppDelegate.mm
│ │ ├── Assets.xcassets
│ │ │ ├── AppIcon.appiconset
│ │ │ │ └── Contents.json
│ │ │ └── Contents.json
│ │ ├── Base.lproj
│ │ │ ├── LaunchScreen.storyboard
│ │ │ └── Main.storyboard
│ │ ├── Info.plist
│ │ ├── main.mm
│ │ ├── SceneDelegate.h
│ │ ├── SceneDelegate.mm
│ │ ├── ViewController.h
│ │ └── ViewController.mm
│ └── xptuto.xcodeproj
│ ├── project.pbxproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ ├── xcshareddata
│ │ │ └── IDEWorkspaceChecks.plist
| ...
├── README.md
├── run_djinni.py
└── web
├── CMakeLists.txt
├── index.html
└── web_glue.cpp
Create the projects in your IDEs
We can get started by creating a project in XCode, another one in Android studio and another one for web. These three projects are contained in a top level folder where we put a top level CMake configuration, the common source code and the common dependencies.
Our Xcode project (in the ios subfolder) is a simple iOS project, the only modifications we have done here is that we have set the C++ standard (Apple calls it “dialect”) to C++ 17. After that we have renamed all the .m files to .mm so they are compiled in Objective-C++. The is nothing else to set up now because we will use the C++ code directly. The only thing to do when we start adding C++ code is to add the files to our XCode target — to our app, namely. Xcode uses its own build system that is not integrated with CMake. For this part, we have no C++ code, so we can stop here.
Our Android project is more complex to set up because Android requires JNI for the Java to talk to the C++, so we have to add our generated JNI bits, Java and C++ to our build.
We need to add this to our build.gradle
sourceSets {
main {
java.srcDirs = ['src/main/gen_java', 'src/main/java']
}
}
Java source sets in gradle, C++ directory in CMake. We also need to add the C++ dependencies to the build already: the Djinni support library is used to implement the JNI bindings. The C++ common source directory will be added later as it is empty now.
The Android CMakeLists.txt needs to include the two needed subfolders: common_src and deps:
# when we have some sources
#add_subdirectory(common_src)
add_subdirectory(deps)
These are symlinked to the actual folders on top of the project, this way the Android project looks self contained. Symlinks are fine in git, I am not sure how they work on Windows.
For Android, we also need to configure the JNI generator, we use Djinni from Dropbox (and we use it _only_ for Java), the run_djinni.py script has some configuration like package name, namespace name… These can be configured ad-hoc.
# Generate
djinni_cmd = ("{base_dir}/deps/djinni/src/run-assume-built "
"--cpp-out {temp_out}/cpp "
"--cpp-namespace {cpp_namespace} "
"--java-out {temp_out}/java "
"--java-package {java_package} "
"--jni-out {temp_out}/jni "
"--idl {in_file} ")
Our web project is fairly simple too so far, but we need to setup emscripten first. For this, you can follow the explanations here: https://emscripten.org/docs/getting_started/downloads.html
Like Android, the build is done with CMake, so it will build and link the common source when we build it. We can code from the IDE, We just need do tell it to select the emscripten toolchain, when loading cmake: add -DCMAKE_TOOLCHAIN_FILE=<EmscriptenRoot>/cmake/Modules/Platform/Emscripten.cmake to the Cmake run command.
Apparently QtCreator has some support for it too https://doc.qt.io/qtcreator/creator-setup-webassembly.html but I have not spent time trying it.
The top level CMake code looks like this so far:
cmake_minimum_required(VERSION 3.4.1)
project(xptuto)set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)if(EMSCRIPTEN)
include_directories("${EMSCRIPTEN_PREFIX}/system/include")
add_subdirectory(web)
endif(EMSCRIPTEN)# when we have sources
# add_subdirectory(common/src)
Finally, we also have a simple C++ project, buildable from the top level directory, which for now will just build the common code into a shared library on our computer. We will use this later for unit testing. The top level C++ project can be loaded in any IDE that supports CMake (CLion, QtCreator…). I use CLion because I am used to the JetBrains products but it comes with a fee.
Develop, Run and Debug
In order to develop, you can switch between the IDEs, depending on what platform you feel like targeting that day.
On iOS, if you add a C++ file from within XCode or AppCode, it will be added to the project, so you can just run the project and the new files will be compiled in. If you add those files from elsewhere, you will need to add them to the XCode project manually (right click on common_src, add files to project and select your new files). Debugging is done as usual from within XCode or Appcode. The debugger understands Objective-C objects and C++ objects without difference, exceptions too, everything is in the same context.
C++ exceptions are… C++ exceptions, and need to be caught with try {} catch {}, not with @try{ } @catch{}
You can see it from this screenshot, self is a UIViewController (an Objective-C class) user is a xptuto::User a C++ class.
On Android, you will need to synchronise CMake by running Build -> refresh C++ projects, once refreshed, running the project on your target will include the new files, you need to run this step every time a new C++ implementation file is added (a .cpp), not when it is changed. New Java JNI bindings are seen automatically by gradle when you run again.
Debugging is a bit more tricky: Android Studio spawns two debuggers, one for Java and another for C++, they have little UI differences and are side by side in the debug window, you cannot drill through Java code to C++ code (step in, e.g.), you need breakpoints at both ends. This is not super convenient but works well enough. Debugging Java with JNI native code in IntelliJ is done the same way I believe.
Djinni will make exceptions bubble up, but when they cross the Java / C++ boundaries, they are transformed into basic RuntimeException or djinni::jni_exception (depending on which way we go) and the stack is lost. So in your exception stack you will see the last Java frame before the C++ or the last C++ frame before the Java.
We can see here the two debugger tabs. We view the Java part now.
On web you need to synchronise CMake, from your build directory, run cmake -DCMAKE_TOOLCHAIN_FILE=<EmscriptenRoot>/cmake/Modules/Platform/Emscripten.cmake -DCMAKE_BUILD_TYPE=Debug .. or build it in CLion, it will automatically reload CMake changes or you can do it manually.
To run on web, you need to start the python http server (or another server but our example uses that) and load localhost:8080 in your browser.
python2 -m SimpleHTTPServer 8080
Debugging can be done in Chrome but values of variables are not visible, breakpoints work. Exceptions bubble up in Javascript and the stack is not lost so you can deduce where your exception came from by reading the names of the frames.
Never manually add files to the CMakeLists here, they are loaded with GLOBs (* recursively). This is just the way this example project has been setup.
Conclusion
Now we have 3 projects that we can run and debug, that all see our common code (which is not there yet). We possibly have saved some time or not, but we certainly did not waste any over creating 3 projects with 3 different programmers.
We can now move on to the interesting part of passing data around.
Next: Pass data back and forth ⇨
Also read:
Introduction:
We present our technical choice.
2. Pass objects around
In this one we will show how to pass objects around from the business logic layer to the various view layers.
3. Unit test
Here we setup unit tests in the C++ code using the commonly used Google Test framework.
4. ReST Client
We implement a minimal ReST client using the platform HTTP implementations.
5. Multi-Threading
Yes, you can use threads in all three environments in a portable way!
6. Offline data with SQLite
Adding SQLite database is simple as ever.