Skip to content

Android Development

Complete guide for building C++ libraries for Android with CCGO.

Overview

CCGO provides comprehensive Android support with:

  • Multi-architecture builds: arm64-v8a, armeabi-v7a, x86, x86_64
  • AAR packaging: Ready-to-use Android Archive format
  • Gradle integration: Seamless integration with Android Studio projects
  • JNI support: Automatic JNI wrapper generation
  • Maven publishing: Publish to Maven Local, Central, or private repositories
  • Docker builds: Build without local Android SDK/NDK installation

Prerequisites

Option 1: Local Development

Required: - Android SDK (API level 21+) - Android NDK (r21+, recommended: r25+) - CMake (3.18+) - Python (3.8+)

Installation:

# Install Android Studio (includes SDK)
# Download from https://developer.android.com/studio

# Set environment variables
export ANDROID_HOME=$HOME/Android/Sdk
export ANDROID_NDK=$ANDROID_HOME/ndk/25.2.9519653

# Verify installation
ccgo check android --verbose

Option 2: Docker-Based Development

Required: - Docker Desktop

# Build with Docker (no local SDK/NDK needed)
ccgo build android --docker

First build downloads prebuilt image (~3.5GB, 5-10 minutes). Subsequent builds use cached image.

Quick Start

Create New Project

# Create new Android-compatible project
ccgo new my-android-lib
cd my-android-lib/my-android-lib

# Build for Android
ccgo build android

Build Single Architecture

# Build for arm64-v8a only
ccgo build android --arch arm64-v8a

Build Multiple Architectures

# Build for arm64-v8a and armeabi-v7a
ccgo build android --arch arm64-v8a,armeabi-v7a

# Build all architectures (default)
ccgo build android

Build Options

# Release build (optimized)
ccgo build android --release

# Debug build (with symbols)
ccgo build android --debug

# Clean build
ccgo build android --clean

# Docker build
ccgo build android --docker

# Link type control
ccgo build android --build-as static   # Static library only
ccgo build android --build-as shared   # Shared library only
ccgo build android --build-as both     # Both (default)

Output Structure

After building, find artifacts in target/android/:

target/android/
├── MY-ANDROID-LIB_ANDROID_SDK-1.0.0.zip      # Main package
├── MY-ANDROID-LIB_ANDROID_SDK-1.0.0-SYMBOLS.zip  # Debug symbols
└── build_info.json                            # Build metadata

Main Package Structure

MY-ANDROID-LIB_ANDROID_SDK-1.0.0.zip
├── lib/
│   ├── static/
│   │   ├── armeabi-v7a/
│   │   │   └── libmy-android-lib.a
│   │   ├── arm64-v8a/
│   │   │   └── libmy-android-lib.a
│   │   ├── x86/
│   │   │   └── libmy-android-lib.a
│   │   └── x86_64/
│   │       └── libmy-android-lib.a
│   └── shared/
│       ├── armeabi-v7a/
│       │   └── libmy-android-lib.so
│       ├── arm64-v8a/
│       │   └── libmy-android-lib.so
│       ├── x86/
│       │   └── libmy-android-lib.so
│       └── x86_64/
│           └── libmy-android-lib.so
├── haars/
│   └── my-android-lib-release.aar
├── include/
│   └── my-android-lib/
│       ├── my-android-lib.h
│       └── version.h
└── build_info.json

Symbols Package Structure

MY-ANDROID-LIB_ANDROID_SDK-1.0.0-SYMBOLS.zip
└── obj/
    ├── armeabi-v7a/
    │   └── libmy-android-lib.so  # Unstripped with debug symbols
    ├── arm64-v8a/
    │   └── libmy-android-lib.so
    ├── x86/
    │   └── libmy-android-lib.so
    └── x86_64/
        └── libmy-android-lib.so

Configuration

CCGO.toml

Configure Android-specific settings:

[package]
name = "my-android-lib"
version = "1.0.0"

[library]
type = "both"  # Build both static and shared libraries

[android]
min_sdk_version = 21          # Android 5.0 (Lollipop)
target_sdk_version = 33       # Android 13
ndk_version = "25.2.9519653"  # Specific NDK version
stl = "c++_static"            # STL type: c++_static or c++_shared
architectures = ["arm64-v8a", "armeabi-v7a", "x86_64"]  # Optional: limit architectures

[build]
cpp_standard = "17"
compile_flags = ["-Wall", "-Wextra"]

Android Configuration Options

Option Type Description Default
min_sdk_version integer Minimum Android API level 21
target_sdk_version integer Target Android API level 33
ndk_version string Specific NDK version Latest installed
stl string STL type: c++_static, c++_shared c++_static
architectures array Target ABIs to build All supported

Supported Architectures

ABI Architecture Description
arm64-v8a ARM 64-bit Modern Android devices (recommended)
armeabi-v7a ARM 32-bit Legacy Android devices
x86_64 Intel 64-bit Emulators, tablets, Chrome OS
x86 Intel 32-bit Legacy emulators

Recommendation: Build arm64-v8a and armeabi-v7a for production apps.

AAR Integration

Using AAR in Android Project

1. Copy AAR to project:

# Copy AAR from build output
cp target/android/MY-ANDROID-LIB_ANDROID_SDK-1.0.0.zip .
unzip MY-ANDROID-LIB_ANDROID_SDK-1.0.0.zip
cp haars/my-android-lib-release.aar android-app/libs/

2. Configure app/build.gradle.kts:

android {
    // ...
}

dependencies {
    implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar"))))
    // or
    implementation(files("libs/my-android-lib-release.aar"))
}

3. Use in Java/Kotlin:

class MainActivity : AppCompatActivity() {
    companion object {
        init {
            System.loadLibrary("my-android-lib")
        }
    }

    // Declare native methods
    external fun nativeMethod(): String

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Call native method
        val result = nativeMethod()
        Log.d("Native", "Result: $result")
    }
}

JNI Integration

Automatic JNI Wrapper

CCGO can generate JNI wrappers automatically:

C++ Header (include/my-android-lib/my-android-lib.h):

#pragma once

#include <string>

namespace my_android_lib {

class MyLib {
public:
    static std::string get_version();
    static int calculate(int a, int b);
};

} // namespace my_android_lib

Generated JNI Wrapper (auto-generated):

#include <jni.h>
#include "my-android-lib/my-android-lib.h"

extern "C" {

JNIEXPORT jstring JNICALL
Java_com_example_mylib_MyLib_getVersion(JNIEnv* env, jclass) {
    std::string version = my_android_lib::MyLib::get_version();
    return env->NewStringUTF(version.c_str());
}

JNIEXPORT jint JNICALL
Java_com_example_mylib_MyLib_calculate(JNIEnv*, jclass, jint a, jint b) {
    return my_android_lib::MyLib::calculate(a, b);
}

} // extern "C"

Manual JNI Implementation

Create src/jni/my_jni.cpp:

#include <jni.h>
#include <string>
#include "my-android-lib/my-android-lib.h"

extern "C" {

JNIEXPORT jstring JNICALL
Java_com_example_MyNativeLib_stringFromJNI(JNIEnv* env, jobject /* this */) {
    std::string hello = my_android_lib::MyLib::get_version();
    return env->NewStringUTF(hello.c_str());
}

JNIEXPORT jint JNICALL
Java_com_example_MyNativeLib_add(JNIEnv*, jobject, jint a, jint b) {
    return my_android_lib::MyLib::calculate(a, b);
}

} // extern "C"

Java/Kotlin side:

package com.example

class MyNativeLib {
    external fun stringFromJNI(): String
    external fun add(a: Int, b: Int): Int

    companion object {
        init {
            System.loadLibrary("my-android-lib")
        }
    }
}

Gradle Integration

Using CCGO Gradle Plugins

CCGO provides Gradle convention plugins for standardized Android builds.

settings.gradle.kts:

pluginManagement {
    repositories {
        google()
        mavenCentral()
        gradlePluginPortal()
    }
}

dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
    }
}

app/build.gradle.kts:

plugins {
    id("com.android.library")
    id("com.mojeter.ccgo.gradle.android.library")
    id("com.mojeter.ccgo.gradle.android.library.native")
}

android {
    namespace = "com.example.mylib"
    compileSdk = 33

    defaultConfig {
        minSdk = 21
    }

    ndkVersion = "25.2.9519653"
}

ccgoNative {
    projectPath.set(file("../"))  // Path to CCGO project root
    buildType.set("release")       // or "debug"
    architectures.set(listOf("arm64-v8a", "armeabi-v7a"))
}

Publishing

Maven Local (Development)

# Publish to Maven Local for testing
ccgo publish android --registry local

# Location: ~/.m2/repository/com/example/my-android-lib/1.0.0/

Maven Central (Production)

1. Configure credentials:

Create ~/.gradle/gradle.properties:

mavenCentralUsername=your-username
mavenCentralPassword=your-password
signing.keyId=12345678
signing.password=your-key-password
signing.secretKeyRingFile=/Users/you/.gnupg/secring.gpg

2. Publish:

ccgo publish android --registry official

Private Maven Repository

ccgo publish android --registry private \
    --url https://maven.example.com/releases

Using Published Library

app/build.gradle.kts:

dependencies {
    implementation("com.example:my-android-lib:1.0.0")
}

Advanced Topics

Multi-Module Projects

Project structure:

my-project/
├── CCGO.toml
├── lib1/
│   ├── CCGO.toml
│   └── src/
└── lib2/
    ├── CCGO.toml (depends on lib1)
    └── src/

lib2/CCGO.toml:

[dependencies]
lib1 = { path = "../lib1" }

Custom CMake Configuration

CMakeLists.txt:

cmake_minimum_required(VERSION 3.18)

# CCGO automatically provides:
# - ${CCGO_CMAKE_DIR}: Path to CCGO cmake utilities
# - ${ANDROID_ABI}: Current architecture being built
# - ${ANDROID_PLATFORM}: Android API level

include(${CCGO_CMAKE_DIR}/CMakeUtils.cmake)

# Custom Android-specific configuration
if(ANDROID)
    # Add Android-specific compiler flags
    add_compile_options(-fPIC)

    # Link against Android libraries
    find_library(LOG_LIB log)
    find_library(ANDROID_LIB android)

    target_link_libraries(${PROJECT_NAME}
        ${LOG_LIB}
        ${ANDROID_LIB}
    )
endif()

Proguard Rules

Create proguard-rules.pro:

# Keep native methods
-keepclasseswithmembernames class * {
    native <methods>;
}

# Keep JNI exported methods
-keep class com.example.mylib.** { *; }

App Size Optimization

1. Strip unneeded symbols (automatic in release builds):

ccgo build android --release

2. Use only required architectures:

[android]
architectures = ["arm64-v8a"]  # Drop 32-bit support if not needed

3. Enable link-time optimization:

[build]
link_flags = ["-flto"]

4. Split APKs by ABI:

app/build.gradle.kts:

android {
    splits {
        abi {
            isEnable = true
            reset()
            include("arm64-v8a", "armeabi-v7a")
            isUniversalApk = false
        }
    }
}

Troubleshooting

Common Issues

NDK Not Found

Error: Android NDK not found

Solution:

# Install NDK via Android Studio: Tools → SDK Manager → SDK Tools → NDK

# Or set manually
export ANDROID_NDK=$ANDROID_HOME/ndk/25.2.9519653

# Verify
ccgo check android --verbose

Architecture Mismatch

Error: UnsatisfiedLinkError: dlopen failed: library "libmy-android-lib.so" not found

Solution:

Ensure AAR contains the architecture your device/emulator uses:

# Check AAR contents
unzip -l my-android-lib-release.aar | grep "\.so$"

# Rebuild with correct architecture
ccgo build android --arch arm64-v8a

C++ Standard Mismatch

Error: undefined reference to std::__cxx11::...

Solution:

Ensure consistent C++ standard across dependencies:

[build]
cpp_standard = "17"  # Match with dependencies

[android]
stl = "c++_static"  # Or c++_shared

Missing Symbols

Error: undefined reference to 'my_function'

Solution:

Check that all source files are compiled:

# Enable verbose build
ccgo build android --verbose

# Check CMakeLists.txt includes all sources

Docker Build Issues

Docker Not Running

Error: Cannot connect to the Docker daemon

Solution:

# Start Docker Desktop
open -a Docker  # macOS

# Verify
docker ps

Image Pull Failure

Error: failed to pull image ccgo-builder-android

Solution:

# Retry with manual pull
docker pull ccgogroup/ccgo-builder-android:latest

# Or use local build
cd ccgo/dockers/
docker build -t ccgo-builder-android -f Dockerfile.android .

Performance Issues

Slow First Build

Normal: First build compiles all dependencies (~10-30 minutes).

Optimization:

# Use prebuilt dependencies (future feature)
ccgo install --prebuilt

# Enable ccache
export USE_CCACHE=1
export CCACHE_DIR=$HOME/.ccache

Incremental Build Not Working

# Clean CMake cache
rm -rf cmake_build/android/

# Rebuild
ccgo build android

Best Practices

1. Version Management

[package]
version = "1.0.0"  # Update before release
# Create git tag
ccgo tag v1.0.0 --push

2. Architecture Selection

[android]
# Production: arm64-v8a + armeabi-v7a (covers 99%+ devices)
architectures = ["arm64-v8a", "armeabi-v7a"]

# Development: arm64-v8a only (faster builds)
# architectures = ["arm64-v8a"]

3. STL Choice

[android]
# Prefer c++_static (no runtime dependency)
stl = "c++_static"

# Use c++_shared only if multiple native libraries share STL
# stl = "c++_shared"

4. Dependency Management

[dependencies]
# Pin to specific versions for reproducibility
spdlog = { git = "https://github.com/gabime/spdlog.git", tag = "v1.12.0" }

# Use CCGO.lock for exact dependency resolution

5. Testing

# Build and test locally
ccgo build android --arch arm64-v8a
ccgo test android

# Test AAR in sample app before publishing

6. CI/CD

# .github/workflows/android.yml
name: Android Build

on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Build Android
        run: |
          pip install ccgo
          ccgo build android --docker

      - name: Test
        run: ccgo test android --docker

      - name: Upload artifacts
        uses: actions/upload-artifact@v3
        with:
          name: android-libs
          path: target/android/*.zip

Examples

Complete Project

See ccgo-now for a complete Android project example.

Minimal Example

// include/mylib/mylib.h
#pragma once
#include <string>

namespace mylib {
    std::string get_greeting();
    int add(int a, int b);
}

// src/mylib.cpp
#include "mylib/mylib.h"

namespace mylib {
    std::string get_greeting() {
        return "Hello from C++!";
    }

    int add(int a, int b) {
        return a + b;
    }
}

Build:

ccgo build android --arch arm64-v8a

Use in Android:

class MainActivity : AppCompatActivity() {
    init {
        System.loadLibrary("mylib")
    }

    external fun getGreeting(): String
    external fun add(a: Int, b: Int): Int

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val greeting = getGreeting()
        val result = add(2, 3)

        Log.d("MyLib", "$greeting, 2+3=$result")
    }
}

Resources

Official Documentation

CCGO Documentation

Community

Next Steps