Android
By the end of this guide your own C program runs on an Android phone as an installable app.
Android needs one thing explained before anything else: it never starts a native executable. That single fact shapes the whole build, so the first section is about it.
Understand why the result is a library, not a program
An APK names a shared library in its manifest. Android loads that library and
calls ANativeActivity_onCreate inside it - that symbol is the entry point,
not main. Natiny provides it, and it starts the thread that calls your
main.
So your program and the engine have to end up in one .so file:
main.c + libnatiny_android_arm64.a → libhello.so → hello.apk
You write ordinary C with a normal main. Nothing about your source is
Android-specific.
Install the compiler and the packaging tools
You need the NDK for the compiler, the SDK build-tools and one platform for packaging, and a JDK 17 or newer for the signer:
sdkmanager "ndk;28.2.13676358" "build-tools;35.0.0" "platforms;android-35"
sdkmanager --licenses
export ANDROID_HOME="$HOME/Android/Sdk"
Another NDK works as well; only the version in the paths below changes.
Connect a phone with USB debugging on, or start an emulator, and check it is there:
adb devices
Unzip the SDK and put your program beside it
Download the SDK bundle for android-arm64:
my_game/
├── include/
│ └── natiny.h
├── lib/
│ └── libnatiny_android_arm64.a
├── android/
│ ├── pack.py
│ ├── AndroidManifest.xml.in
│ └── res/
└── main.c
There is no shared library of Natiny's in the bundle, and that is deliberate:
the .so Android loads is the one you are about to create from the engine and
your program together.
Write a program you will recognise on screen
The window always covers the screen, so the size you ask for is ignored - use
natiny_window_get_width and natiny_window_get_height for the real one. A
green square on a dark background is enough to prove the whole chain worked.
#include "natiny.h"
#include <stddef.h>
static NatinyWindow window;
static void frame(float dt, void* userdata)
{
(void)dt;
(void)userdata;
natiny_window_bind(window);
natiny_render_clear(0.07f, 0.09f, 0.13f, 1.0f);
natiny_render_set_color(0.66f, 0.76f, 0.22f, 1.0f);
natiny_render_rectangle(360.0f, 260.0f, 80.0f, 80.0f);
natiny_window_unbind();
}
int main(void)
{
if (!natiny_backend_init(NATINY_BACKEND_AUTO)) {
return 1;
}
window = natiny_window_create("Hello, Natiny", 800, 600);
if (!window) {
natiny_backend_shutdown();
return 1;
}
natiny_backend_loop(frame, NULL);
natiny_backend_shutdown();
return 0;
}
NATINY_BACKEND_AUTO selects Vulkan, the only backend Android has.
Link your program and the engine into one library
The NDK's compiler is named after its target: the ABI and the minimum API level are both in the file name, so no flags are needed for either.
NDK_BIN="$ANDROID_HOME/ndk/28.2.13676358/toolchains/llvm/prebuilt/linux-x86_64/bin"
Use darwin-x86_64 on a Mac and windows-x86_64 on Windows.
"$NDK_BIN/aarch64-linux-android24-clang" -O2 -fPIC \
-Iinclude -c main.c -o main.o
"$NDK_BIN/aarch64-linux-android24-clang" -shared main.o \
lib/libnatiny_android_arm64.a -o libhello.so \
-u ANativeActivity_onCreate -landroid -llog -ldl -lm
Two of those options are not obvious:
-sharedmakes the.sothat Android loads, rather than an executable that nothing would start.-u ANativeActivity_onCreatekeeps the entry point. Nothing on the link line refers to it - Android finds it by name at run time - so without-uthe linker drops it, and the app installs and then fails at startup.
Check that it survived, with the NDK's own nm - a host nm cannot always
read an aarch64 library:
"$NDK_BIN/llvm-nm" -D libhello.so | grep ANativeActivity_onCreate
A line with T ANativeActivity_onCreate means the symbol is exported. No
output means -u is missing.
Pack the library into an APK
pack.py builds the manifest, adds the resources, signs and aligns. Create a
folder for your data even if the program reads nothing yet:
mkdir -p assets
python android/pack.py --project ./assets --lib libhello.so \
--abi arm64-v8a --package com.example.hello --name "Hello" \
--output hello.apk
adb install -r hello.apk
--abi has to match the compiler you used: arm64-v8a goes with
aarch64-linux-android24-clang. For an x86_64 emulator, take the
android-amd64 SDK, compile with x86_64-linux-android24-clang, and pass
--abi x86_64.
Without --keystore the packer makes a debug key at ~/.natiny/debug.keystore
on first use and reuses it, which is what keeps adb install -r working
between builds. Use a real key for anything you publish, and keep it: Google
Play ties an application id to the key that first signed it.
Run it and read what it prints
Open Hello from the launcher. A dark screen with a green square means Android loaded your library, called Natiny's entry point and reached your frame callback.
When it closes instead, the log says why:
adb logcat -s natiny
Read and write files from the app
Whatever you pass to --project becomes assets/data/ inside the APK, and
that is what the resource API reads:
assets/
└── player.png
NatinyResource player = natiny_resource_load("data/player.png");
That is the same string a desktop build uses. APK assets are read-only and
have no filesystem path, so they can only be read through the resource API. A
relative path given to natiny_resource_save goes to the app's private
directory, and natiny_resource_load looks there too.
Typical problems
- The app installs and dies at startup with no log -
-u ANativeActivity_onCreateis missing from the link line. INSTALL_FAILED_NO_MATCHING_ABIS---abidoes not match the device, or does not match the compiler you used.cannot find the Android SDK- setANDROID_HOME, or pass--sdk.INSTALL_FAILED_UPDATE_INCOMPATIBLE- the same application id is installed under a different key. Uninstall it or reuse the key.- A device older than Android 7.0 cannot run it. API 24 is the release where Vulkan is required of a device, and Vulkan is the backend.
What you have now
libhello.so holds your program, the engine and the entry point Android looks
for, and hello.apk is that library with your data, a manifest and a
signature around it. The C source and the resource paths are the ones a
desktop build uses.
Two things about the platform are worth knowing next. A touch arrives as the left mouse button, plus a callback that reports every finger. And when Android sends your app to the background it destroys the window you were drawing to: Natiny reports that as window iconification, and drawing after it is a crash. Stop inside that callback.