JUCE + CMake + Android, now works

(This is a translation from my Japanese blog post)

JUCE6 supports CMake, but there is one missing platform: Android.

You might say "huh?" -- CMake has been supported by Android Studio for a long time, and it should be the best platform that would benefit at most. Win/Mac/Linux/iOS are not CMake native build environments, while supported in JUCE, Android, which supports CMake as a native build environment, is somehow not supported. This has to be fixed.

Missions

As of the January 2021 release of JUCE 6.0.6, Android is only supported via Projucer. It generates Gradle projects for Android applications that can be then built on and launched from Android Studio. App developers can launch Android Studio from Projucer, and there is a debuggable application opened at ready state. The JUCE application part will be a natively coded application using the Android NDK, and the Projucer-generated project renders the UI on JUCE's own View using native code, and interoperates UI events between the Android framework and the app native code.

In Projucer-generated Android applications, various information in AndroidManifest.xml and build.gradle are the customizible properties defined in Projucer's AndroidExporter. But it is better for Android application developers to be able to adjust these items on their own, so that the code can stay more straightforward, maintainable, and in higher quality. The project generated by Projucer is unnatural, old-fashioned, and falls into anti-patterns in some parts. It's hard to make desired changes to manifest, build scripts etc. on .jucer. ("How can I add new <query> element which is new in Android 11?")

If the JUCE team were of plentiful Android development experts who can keep up with the latest Android development trends and implement proper project model generation in Projucer, that would be nice. But it is unlikely to happen, as it didn't happen for CMake support. I don't believe that generating everything via a tool like Projucer is great either. For better Android support, people should migrate to CMake's model and it should be (made) possible, so that normal Android app developers can make desired changes at their own will.

The CMake support feature does not have to exist to replace Projucer as a fully automatic application generation mechanism. Android C++/NDK projects are already based on built-in CMake support, therefore what is expcted for JUCE + Android + CMake is how natural fit it can be to apply JUCE CMake projects into the CMake support in Android Studio (Gradle) projects.

As a build mechanism for Android applications without Projucer, the mission here is accomplished if you can call the entry point function of the native code from the application's Activity that goes around and into the bootstrapping process of the JUCE application. (In addition to that, we also need to manage the state of the application according to the Android application lifecycle, but this is not a problem specific to JUCE applications, so we don't need to worry about it here. Projucer is not great at that aspect either way.)

In order to achieve this, we have to understand what kind of Android application files are generated by Projucer, and find out how to establish a feasible workflow in which users (Android application developers) can create what and how to connect JUCE applications.

Ideally, if "existing" CMake projects can be imported as they are, it would be great as it brings greater opportunity to port them to Android. Unfortunately this seems not possible at present, but it is still better to stick to "minimize the diff from existing CMake projects" principle. Yet, I don't plan to spend too much development resource for tooling to achieve extreme diff minimization; the maintainability of such a tool itself would not be great on the other hand.

Based on the premises above, I have set the following two goals for this project.

  • build and run the Android version with the file structure of JUCE+CMake+VSCode template and its GitHub repository (The article is written in Japanese that you would like to skip...)
  • build and run the standalone version of the plugin configured with CMake on Android: this time, target witte/Eq

In case you are (only) interested in the accomplished projects, you can check out the following repositories:

The latter project in particular contains a patch from the original github repo, so it would be easier to see how small the diff for Android builds is. The entire latter project also includes my own code for my audio plugin framework, so the actual diff for an ordinary Android port would be even smaller.

Analysis

This section is a summary of what I found out to accomplish this mission. If you're not interested in the sausage internals, you can skip it (in case you can keep reading without figuring out why we're doing this...)

Android C++ support in Android Studio and Projucer

When you create a new project with C++ support in Android Studio, it will look like this image. This is the state we should target to get closer. There are a lot of files under res, so I've collapsed them.

project-android-studio.png

On the other hand, JUCE's Android project looks like this. This is an incomplete list. Specifically, the C++ code is not shown here. It is because they stay outside of the top directory of Builds/Android.

project-projucer.png

There are handful of differences, but as you can see they are similar in general.

Files generated by Projucer's AndroidExporter

In this section, we will inspect the contents of the Android Gradle project generated by Projucer.

The files generated by Projucer would be different for each project type, such as GUI Application, Audio Plugin, etc., but the general structure should be the same.

build.gradle, local.properties, settings.gradle, gradlew(.bat), gradle/

There is nothing special about those files, and they are almost identical to normal Android Studio C++ projects. The version number etc. may be different, and the Maven repository may be added in some preview versions of Android Studio, but what Projucer generates for them is simple.

app/src/debug/res, app/src/release/res

For Android resources, there is only string.xml which defines @string/app_name. However, Projucer's generation of Android resources is a bit awkward and it generates the separate directories for debug and release builds, which is not very common. We don't have to follow Projucer's awkwardness, which has to support everything the .jucer indicates. We are designing nicer CMake project structure from scratch.

app/src/main/AndroidManifest.xml

There are manifest items such as permissions required depending on the modules you use, such as audio recording, wifi and bluetooth. This is something that application developers should work on by themselves.

Since Android manifests can be merged, we would be able to build an AAR for each JUCE module and add AndroidManifest.xml to each one, but we don't have to go that far for now.

app/build.gradle

The most special and meaningful part in this file is the addition of custom sourceSets, which specifies the Java sources in the JUCE standard modules (more on this later). Basically, it is not really appropriate to add things here and some of them should be removed, while some are okay to keep.

There are other things that are generated that are generally unnecessary, such as signingConfig. It is Projucer to blame that tries to provide everything in build.gradle. We don't need those Projucer technical debt in our CMake support. There is no need to generate buildTypes and productFlavors either, and developers who need them should configure those items by themselves at their convenience.

app/CMakeLists.txt

In this file, various options will be added depending on the JUCE module we use.

If the juce_audio_devices module is included, Oboe build will be added:

set(OBOE_DIR "/media/atsushi/extssd0/sources/JUCE/modules/juce_audio_devices/native/oboe")
add_subdirectory (${OBOE_DIR} . /oboe)

I think this is always included, but maybe it's optional:

add_library("cpufeatures" STATIC "${ANDROID_NDK}/sources/android/cpufeatures/cpu-features.c")

set_source_files_properties("${ANDROID_NDK}/sources/android/cpufeatures/cpu-features.c")
    PROPERTIES COMPILE_FLAGS "-Wno-sign-conversion -Wno-gnu-statement-expression")

add_definitions("-DJUCE_ANDROID=1" "-DJUCE_ANDROID_API_VERSION=16" "-DJUCE_PUSH_NOTIFICATIONS=1"
    "-DJUCE_PUSH_NOTIFICATIONS_ACTIVITY=\"com/rmsl/juce/JuceActivity\" "-DJUCER_ANDROIDSTUDIO_7F0E4A25=1"
    "-DJUCE_APP_VERSION=1.0.0" "-DJUCE_APP_VERSION_HEX=0x10000")

include_directories( AFTER
    "... /... /... /JuceLibraryCode"
    "/media/atsushi/extssd0/sources/JUCE/modules"
    "${ANDROID_NDK}/sources/android/cpufeatures"
)
enable_language(ASM)

The contents of add_definitions() depend on the modules and module options. <... >/JUCE/modules above comes from the global path settings on my machine. There is also a property that contains something like the UID of the .jucer project, but it seems unnecessary.

There are additional options depending on your config, but they are too long to list here. Most of them are taken care of by the standard JUCE CMake support:

if(JUCE_BUILD_CONFIGURATION MATCHES "DEBUG")
    add_definitions("-DJUCE_DISPLAY_SPLASH_SCREEN=1" "-DJUCE_USE_DARK_SPLASH_SCREEN=1"  
        "-DJUCE_PROJUCER_VERSION=0x60005" "-DJUCE_MODULE_AVAILABLE_juce_audio_basics=1" ...
        "-ddebug=1" "-d_debug=1")
elseif(JUCE_BUILD_CONFIGURATION MATCHES "RELEASE")
    add_definitions("-DJUCE_DISPLAY_SPLASH_SCREEN=1" "-DJUCE_USE_DARK_SPLASH_SCREEN=1"
        "-DJUCE_PROJUCER_VERSION=0x60005" "-DJUCE_MODULE_AVAILABLE_juce_audio_basics=1" ...
        "-dndebug=1")
else()
message( FATAL_ERROR "No matching build-configuration found." )
endif()

Most of the rest in this CMakeLists.txt uses add_library() to enumerate sources and set their properties. In practice, only source enumeration should be sufficient.

Finally, target_link_libraries() etc. are written. glesv2 (or v3) and egl are probably unnecessary without juce_gui_basics or juce_opengl.

target_compile_options( ${BINARY_NAME} PRIVATE "-fsigned-char" )

if( JUCE_BUILD_CONFIGURATION MATCHES "DEBUG" )
    target_compile_options( ${BINARY_NAME} PRIVATE)
endif()
  
if( JUCE_BUILD_CONFIGURATION MATCHES "RELEASE" )
    target_compile_options( ${BINARY_NAME} PRIVATE)
endif()

find_library(log "log")
find_library(android "android")
find_library(glesv2 "GLESv2")
find_library(egl "EGL")

target_link_libraries( ${BINARY_NAME}
    ${log}
    ${android}
    ${glesv2}
    ${egl}
    "cpufeatures".
    "oboe"
)

JUCE/modules/juce_core/native/javacore/init

It contains only the file init/com/rmsl/juce/Java.java, whose name is awkward. The code is short.

package com.rmsl.juce;
Context. import android.content;
Context; public class Java
{
    static
    {
        System.loadLibrary ("juce_jni");
    }
    public native static void initialiseJUCE (Context appContext);
}

The actual implementation is in JNI in the juce_core module. This JNI callable API must exist, while it does not have to exist in the JUCE module i.e. if you create an equivalent Java class in Kotlin then this code file is unnecessary.

JUCE/modules/juce_core/native/javacore/app

There is only one file, com/rmsl/juce/JuceApp.java. It is short too.

package com.rmsl.juce;
import com.rmsl.juce.Java;
Java; import android.app;
public class JuceApp extends Application
{
    @Override
    public void onCreate()
    {
        super.onCreate();
        Java.initialiseJUCE (this);
    }
}

In fact, this goes against Android best practices and should be replaced: there is only one Application class instance in an Android app, and occupying the precious one with this trivial class is evil. We should modernize it and use Jetpack App Startup instead. And even if you don't, you can (and should) make it a ContentProvider instead of Application.

JUCE/modules/juce_gui_basics/native/javaopt/app

There are two sources here. com/rmsl/juce/JuceActivity.java should be replaced because it makes it impossible to use modern alternatives such as AppCompatActivity. However, since JNI signatures are involved, it would be necessary to verify whether it is possible to call functions equivalent to appNewIntent() and it still works off the rails...

package com.rmsl.juce;

import android.app.Activity;
import android.content.Intent;

//==============================================================================
public class JuceActivity extends Activity
{
    //==============================================================================
    private native void appNewIntent (Intent intent);

    @Override
    protected void onNewIntent (Intent intent)
    {
        super.onNewIntent(intent);
        setIntent(intent);

        appNewIntent (intent);
    }
}

The other file, com/rmsl/juce/JuceSharingContentProvider.java, is rather long and should be included in the application as is. It is a lengthy ContentProvider implementation, which would probably have nothing wrong with its design.

Bootstrapping

The bootstrapping of a JUCE application in Android is as follows.

  • For GUI applications, JuceActivity in juce_gui_basics calls com.rmsl.juce.Java.initialiseJUCE().
    • If you are not using a GUI application, you should follow the same procedure at Service.onCreate(), etc.
  • Java class (at initializer) loads libjuce_jni.so with loadLibrary().
  • Java.initialiseJUCE() is associated with juce_JavainitialiseJUCE() by JNI_OnLoad() with registerNatives() of JNIEnv, and the native Thread:: initialiseJUCE(), which is implemented as a call to Thread::: initialiseJUCE().
    • Not sure why they bothered to name it that (it should be associated with Java_com_rmsl_juce_Java_initialiseJUCE() by default).
  • Thread::initialiseJUCE() calls juce_juceEventsAndroidStartApp() at the end.
  • juce_juceEventsAndroidStartApp() is called by loading the file of the shared library of the running application obtained by juce_getExecutableFile() with dlopen() separately, and then retrieving juce_CreateApplication() from it with dlsym().
  • The juce_events module contains juce_CreateApplication() defined in juce_Initialisation.h.
  • juce_CreateApplication() is defined by the macro JUCE_CREATE_APPLICATION_DEFINE(AppClass), which is defined for each plugin format, but for Android, there is no plugin standard supported by JUCE, only Standalone is supported, and it is included in the generated code.

Implementation

As an Android application template

In order to achieve our goal, we will first create a standard Android application, and then adjust its CMakeLists.txt specified in app/build.gradle to build and load the JUCE application as expected.

The first step is to create a C++ application in Android Studio (I personally create the files from scratch (actually partially copy and paste from my existing app), but it may not be easy). Gradle-related files can be used without any changes. I'm using Android Studio Arctic Fox (Canary) and therefore I specify gradle 6.8-rc-1 and Android Gradle Plugin 7.0.0-alpha04, but you can stay with the stable versions.

app/build.gradle

In the app/build.gradle file, we specify (partially) contents like below. I removed most of the unnecessary stuff such as buildTypes and productFlavors, so the file is much simpler than those generated by Projucer.

defaultConfig {  
  applicationId "com.yourcompany.newproject".  
  minSdkVersion 16  
  targetSdkVersion 30  
  externalNativeBuild {  
    cmake { arguments "-DANDROID_STL=c++_static", "-DANDROID_CPP_FEATURES=exceptions rtti" }  
  }
}  
  
sourceSets { main.java.srcDirs += [
  "... /JUCE/modules/juce_core/native/javacore/app",
  "... /JUCE/modules/juce_core/native/javacore/init",
  "... /JUCE/modules/juce_gui_basics/native/javaopt/app"
  ] }

The sourceSets can be even reduced by using androidx Application Startup or something similar, and it would be more appropriate, but I won't go into that in this article. (If I add them, I'll have to add the Kotlin code and explain them as well.)

app/CMakeLists.txt

The app/CMakeLists.txt file contains the following contents. It's a bit long, but I'll post the whole thing. It is a bit long, but I'll put all of it here. I edited the file which was pulled from the juce_cmake_vscode_example repository, so there are some remnants of it.

# Automatically generated makefile, created by the Projucer.
# Your changes will be overwritten when you re-save the Projucer project!

cmake_minimum_required(VERSION 3.15)

PROJECT(JUCE_CMAKE_ANDROID_EXAMPLE
LANGUAGES C CXX
VERSION 0.0.1
)

# for clang-tidy(this enable to find system header files).
if(CMAKE_EXPORT_COMPILE_COMMANDS)
set(CMAKE_CXX_STANDARD_INCLUDE_DIRECTORIES ${CMAKE_CXX_IMPLICIT_INCLUDE_DIRECTORIES})
endif()

if (ANDROID)

# defs, some are specific to Android and need definitions in prior to `add_subdirectory(JUCE)`.
add_definitions(
    "-DJUCE_ANDROID=1" 
    "-DJUCE_PUSH_NOTIFICATIONS=1" 
    "-DJUCE_PUSH_NOTIFICATIONS_ACTIVITY=\"com/rmsl/juce/JuceActivity\" 
    )

# Enable these lines if you use juce_audio_devices API
set(OBOE_DIR "... /JUCE/modules/juce_audio_devices/native/oboe")
add_subdirectory (${OBOE_DIR} . /oboe)

# libcpufeatures

add_library("cpufeatures" STATIC "${ANDROID_NDK}/sources/android/cpufeatures/cpu-features.c")
set_source_files_properties("${ANDROID_NDK}/sources/android/cpufeatures/cpu-features.c")
  PROPERTIES COMPILE_FLAGS "-Wno-sign-conversion -Wno-gnu-statement-expression")
enable_language(ASM)
endif (ANDROID)

# build JUCE
add_subdirectory("... /JUCE" . /JUCE)

# build App code (e.g. libExamplePlugin_Standalone.so)
add_subdirectory(src/main/cpp)

if (ANDROID)
add_library(juce_jni
    SHARED
    dummy.cpp
    )
target_link_libraries(juce_jni
    ExamplePlugin_Standalone
)
target_compile_options(ExamplePlugin PRIVATE "-fsigned-char" )
endif (ANDROID)

There are two parts enclosed from if (ANDROID) to endif (ANDROID), but other than them, it is the same as CMakeLists.txt on the desktop. In the first part, we specify some constants generated by Projucer as they are. I don't think this application uses push notifications, but I left them in because otherwise the builds failed.

One of the key points in the second part is target_link_libraries(), which links the Standalone build of the plugin project (ExamplePlugin_Standalone). For Android you can only build Standalone (except for the Shared Code build), which will be built as a shared library on Android instead of executable. It is the actual JUCE application, while libjuce_jni.so is supposed to be loaded by explicit name in the application bootstrap. You can rewrite CMakeLists.txt of the application to change the library name from ExamplePlugin to juce_jni, but we want to be able to build the application as it is without making any changes to the original file, so we would build libjuce_jni.so separately.

app/dummy.cpp

One more thing needs to be added to the application file. dummy.cpp is specified in this CMakeLists.txt because CMake won't build it if you don't specify any source in add_library(). An empty file would suffice.

app/src/main/cpp/CMakeLists.txt

The main part of the JUCE application (the contents of the src directory in juce_cmake_vscode_example) should be copied into the directory src/main/cpp in this project. The contents of CMakeLists.txt in this directory has some short additional bits:

if (ANDROID)  
  
# dependencies  
find_library(log "log")  
find_library(android "android")  
find_library(glesv2 "GLESv2")  
find_library(egl "EGL")  
set(cpufeatures_lib "cpufeatures")  
set(oboe_lib "oboe")  
  
target_include_directories( ExamplePlugin PRIVATE  
  "${ANDROID_NDK}/sources/android/cpufeatures"  
    "${OBOE_DIR}/include"  
)  
  
endif (ANDROID)

target_link_libraries(ExamplePlugin PUBLIC
...
${log}  
${android}  
${glesv2}  
${egl}  
${cpufeatures_lib}  
${oboe_lib}
)

It first searches for additional Android-specific libraries with find_library() and then adds them at target_link_libraries().

Also, juce_cmake_vscode_example specifies some SVG file as a binary asset. If we put it in the assets directory, it would be confusing with Android assets, so I made a minor modification to point to a different directory called juce-assets in the juce_add_binary_data() call and placed the SVG file.

app/src/main/AndroidManifest.xml

The last change to make is to AndroidManifest.xml. We will make a few changes to the contents of the <manifest> element.

<supports-screens android:smallScreens="true" android:normalScreens="true" android:largeScreens="true"
   android:anyDensity="true" android:xlargeScreens="true"/>  
  
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>  
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-feature android:glEsVersion="0x00030000" android:required="true" />

<application android:label="@string/app_name" android:hardwareAccelerated="false" android:name="com.rmsl.juce.JuceApp">
 <activity android:name="com.rmsl.juce.JuuceActivity" android:label="@string/app_name"
   android:configChanges="keyboardHidden|orientation|screenSize"  
   android:screenOrientation="userLandscape" android:launchMode="singleTask" 
   android:hardwareAccelerated="true">  
   <intent-filter>  
     <action android:name="android.intent.action.MAIN"/>  
     <category android:name="android.intent.category.LAUNCHER"/>  
   </intent-filter>  
 </activity>.
</application> 

(2020/1/17: added missing android:name in <application> element.)

The classes of Application and Activity are fixed to those from JUCE (although I got rid of JuceApp in my repository; as I explained earlier it is inappropriate). I left <supports-screens> and <uses-feature> as they are generated by Projucer, but they will work without them. Add <uses-permission>s if you need more of those.

Fixing JUCE itself

The application is almost complete already, but if you build and run the application as is, it will just launch a blank Activity that does not display anything. This is because the JUCE itself cannot find juce_CreateApplication() described in the bootstrapping section in the application's shared library.

All JUCE modules are linked as PRIVATE, which is equivalent to -fvisibility=hidden. It seems due to ODR (one definition rule). The juce_CreateApplication() is compiled as part of the resulting library without being stripped, but it is hidden and cannot be found by dlsym(). JUCE will not start the JUCE application loop in such case, the bootstrapping process will then simply end without doing anything. This problem can be fixed with the following one liner patch.

https://gist.github.com/atsushieno/7da120ef87826c9d8fdf8ad6542a16f6

All those changes, you can run JUCE applications built with CMake on Android.

sshot-template-project.png

Porting an existing JUCE plugin application.

https://github.com/atsushieno/aap-juce-witte-eq takes a plugin project witte/Eq which is made with CMake. We specify this application as a submodule, apply a patch to it, and then add the template described earlier -- actualy with a few modifications -- as an add_subdirectory() in app/CMakeLists.txt . As you can see in the patch file, it is basically the same as find_library() and some other changes described earlier. IT also adds Standalone as the target, but it is because, as mentioned earlier, there is no other format supported by JUCE in Android, and it is necessary to add a reference to it as a shared library.

(Although, this plug-in port is rather for my own Android audio plugin framework, and I have added the necessary JUCE modules for this purpose in this patch.)

There are not so many OSS JUCE applications made with CMake yet, but I expect that there can be more applications that can be ported like this.

sshot-app-port.png

Summary

These are the resulting repos for my effort to achieve JUCE + CMake + Android, with a template CMake project and an existing CMake-based plugin. The projects are clean, and making existing CMake-based JUCE apps ready for Android is actually easy once we got to know what changes to make.