はじめに
みなさんこんにちは!
Android開発って、普通KotlinやJavaでやるものですよね。私も最初はそう思ってたんですが、ある時から「もっと速くできないかな?」って思い始めて、Android NDKでC++開発に手を出しました。
最初は正直「難しそう...」って思ってたんですが、やってみると意外と面白くて、特にパフォーマンスの向上が目に見えて感動しました!
この記事では、私が実際にAndroid NDKでC++開発をやってみた経験をもとに、基礎から実践的な実装まで、なるべく分かりやすく書いてみます。
なぜAndroidでC++を使うのか?
パフォーマンスの向上がすごい!
正直、最初は「Kotlinで十分でしょ?」って思ってたんです。でも、実際に重い処理をやってみると、KotlinやJavaはAndroid Runtime(ART)上で動いているからどうしてもオーバーヘッドがあるんですよね。
それに対してC++のネイティブコードは、ハードウェアに直接近い層で動作するから、本当に速いんです!特に以下のような処理では、体感で分かるくらいの差が出ます。
- 画像・動画処理
- 数値計算(数値解析、機械学習)
- ゲームエンジン
- 物理シミュレーション
- リアルタイム音声処理
既存資産の活用ができる!
これも私が使ってみて感動したポイントなんですが、すでにC/C++で書かれた優秀なライブラリを、Androidアプリでそのまま使えるんです。例えば:
- OpenCV(画像処理) - これは本当に便利でした!
- FFmpeg(動画処理) - 動画処理ならこれ一択
- 独自開発したアルゴリズム - 会社のノウハウを活用できます
Android NDKって何?
Android NDK(Native Development Kit)は、Googleが提供しているツールセットです。これを使うと、C/C++で書いたコードをAndroid用の共有ライブラリ(.soファイル)にコンパイルして、Java/KotlinからJNI(Java Native Interface)で呼び出せるようになります。
開発環境のセットアップ
インストール手順
- Android Studioを起動
- SDK Managerを開く(Tools → SDK Manager)
-
SDK Toolsタブで以下にチェックを入れてインストール:
- NDK (Side by side) - これで複数バージョン管理できます
- CMake - ビルドシステムです
まずは簡単なところから始めましょう。新しいプロジェクトを作成する時に「Native C++」テンプレートを選択します。これで基本的な構成は自動で作られます。
プロジェクト構造
app/
├── src/
│ └── main/
│ ├── cpp/
│ │ ├── CMakeLists.txt
│ │ └── native-lib.cpp
│ └── java/
└── build.gradle.kts
native-lib.cpp
これがC++側のネイティブコード実装ファイルです。JNI関数を定義し、Kotlin/Javaから呼び出せるようにします。関数名はJava_パッケージ名_クラス名_メソッド名の命名規則に従う必要があります。extern "C"でC言語リンケージを指定し、JNIEXPORTとJNICALLマクロで可視性と呼び出し規約を設定します。
#include <jni.h>
#include <string>
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_myapp_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
CMakeLists.txt
CMakeはC++コードのビルド設定を記述するファイルです。add_libraryで共有ライブラリ(.soファイル)を定義し、find_libraryでAndroidのシステムライブラリ(ログ機能など)を検索し、target_link_librariesでリンクします。Android Studioが自動的にこのファイルを読み込んでビルドを実行します。
cmake_minimum_required(VERSION 3.22.1)
project("myapp")
add_library(
native-lib
SHARED
native-lib.cpp
)
find_library(
log-lib
log
)
target_link_libraries(
native-lib
${log-lib}
)
MainActivity.kt
Kotlin側でネイティブライブラリを利用するための実装です。System.loadLibrary()でネイティブライブラリを読み込み、externalキーワードでJNI関数を宣言します。この宣言により、C++で実装された関数をKotlinから透過的に呼び出せます。
class MainActivity : AppCompatActivity() {
companion object {
init {
System.loadLibrary("native-lib")
}
}
private external fun stringFromJNI(): String
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// ネイティブメソッドを呼び出し
val nativeString = stringFromJNI()
Log.d("MainActivity", nativeString)
}
}
build.gradle.kts(app)
アプリレベルのGradleビルドファイルにNDK設定を追加します。externalNativeBuildブロックでCMakeLists.txtのパスとバージョンを指定し、defaultConfig内でC++コンパイラフラグを設定できます。これによりGradleがCMakeを呼び出してネイティブコードをビルドします。
android {
compileSdk = 34
defaultConfig {
applicationId = "com.example.myapp"
minSdk = 21
targetSdk = 34
externalNativeBuild {
cmake {
cppFlags("")
}
}
}
externalNativeBuild {
cmake {
path("src/main/cpp/CMakeLists.txt")
version = "3.22.1"
}
}
}
JNIの基本概念
データ型の対応
Java/KotlinとC++間でデータをやり取りする際、JNIの型システムを経由する必要があります。この対応表、最初は覚えるのが大変でしたが、使っているうちに慣れます。基本型は比較的直感的ですが、StringやオブジェクトはJNI APIを使った変換が必要です。
| Java/Kotlin | JNI | C/C++ |
|---|---|---|
| boolean | jboolean | unsigned char |
| byte | jbyte | signed char |
| char | jchar | unsigned short |
| short | jshort | short |
| int | jint | int |
| long | jlong | long long |
| float | jfloat | float |
| double | jdouble | double |
| String | jstring | - |
関数命名規則
JNIの関数名は特定のルールがあって、これを間違えると動かないので注意です:
Java_パッケージ名_クラス名_メソッド名
私も最初はこの命名でハマりました。パッケージ名のドットがアンダースコアに変わるので気をつけてください。
例:
Java_com_example_myapp_MainActivity_stringFromJNI
WindowsCE から Android への移植
WindowsCEは1996年にMicrosoftが発表した組み込み機器向けOSで、2000年代には産業用端末、POS端末、医療機器、カーナビなど幅広い分野で採用されました。特に日本国内では多くの業務用ハンディターミナルがWindowsCE上で稼働していました。
しかし、2013年にMicrosoftがWindows Embedded Compact 2013(WindowsCE 8.0)をリリースした後、新バージョンの開発は停止し、2023年10月にメインストリームサポートが終了しました。一方、2008年に登場したAndroidは急速にシェアを拡大し、現在では組み込み機器やエンタープライズ市場でもWindowsCEの後継プラットフォームとして採用されています。
このような背景から、長年稼働してきたWindowsCE向けの資産(特にC++で実装されたビジネスロジックやアルゴリズム)をAndroidに移植するニーズが高まっています。幸いなことに、両プラットフォームともC++を使用できるため、適切な移植作業により既存コードを再利用できます。
プラットフォーム固有の課題
既存のWindowsCE向けC++ライブラリをAndroidに移植する際には、以下のような課題に直面することがあります。
アーキテクチャの違い
WindowsCEは主にARM(32bit)アーキテクチャで動作していましたが、Androidは現在ARM64(64bit)が主流です。ポインタサイズやアラインメント要件が異なるため、ポインタを整数型にキャストするコードや構造体のメモリレイアウトに依存するコードは注意が必要です。armeabi-v7a(32bit)とarm64-v8a(64bit)の両方をサポートする場合、条件付きコンパイルが必要になることがあります。
エンディアンの考慮
WindowsCEとAndroidは両方ともリトルエンディアンが一般的ですが、バイナリファイルやネットワークプロトコルを扱う場合、エンディアン変換が正しく実装されているか確認が必要です。ビッグエンディアンのデータを扱うコードがある場合、htonl()/ntohl()などの関数がAndroidでも同様に動作するか検証しましょう。
メモリマップファイル
WindowsCEのCreateFileMapping()、MapViewOfFile()といったメモリマップファイルAPIは、AndroidではPOSIXのmmap()/munmap()を使用します。基本的な概念は同じですが、フラグや保護属性の指定方法が異なります。大きなファイルを効率的にアクセスする際には有用ですが、Androidのメモリ制約を考慮してマップサイズを適切に管理する必要があります。
ヒープ管理
WindowsCEのHeapCreate()、HeapAlloc()といった独自のヒープ管理APIは、Androidでは標準C++のnew/deleteやC言語のmalloc()/free()を使用します。カスタムメモリアロケータを実装している場合、Androidのメモリ管理特性(ページサイズ、メモリプレッシャー通知)に合わせて最適化が必要です。メモリリークを検出するツールもWindowsのものとは異なります。
ファイルシステムAPI
WindowsCEのファイルシステムAPI(CreateFile()、ReadFile()など)はWindowsカーネルベースですが、AndroidはLinuxカーネルでPOSIX標準API(open()、read()など)を使用します。パス区切り文字も\から/に変わります。また、Androidでは外部ストレージへのアクセスにランタイムパーミッションが必要で、scoped storageの制約もあるため、ファイルアクセス戦略の見直しが必要です。
スレッドAPI
WindowsCEのスレッドAPI(CreateThread()、WaitForSingleObject()など)はWin32ベースですが、Androidでは標準C++11のスレッドライブラリ(std::thread、std::mutex)またはPOSIX Threads(pthread)を使用します。クリティカルセクションはstd::mutexに、イベントオブジェクトはstd::condition_variableに置き換える必要があります。スレッドローカルストレージの実装方法も異なります。
描画API
WindowsCEのGDI(Graphics Device Interface)はAndroidには存在しません。画面描画が必要な場合、Android側でJetpack ComposeやCanvasを使用するか、ネイティブ側でOpenGL ES、Vulkan、またはAndroid NDKのNativeActivityを使用します。単純なビットマップ操作であればJNI経由でBitmapオブジェクトをやり取りすることも可能です。描画ロジックをネイティブに残す場合、プラットフォーム非依存のレンダリングエンジン(OpenGL ES 3.0以降推奨)への移行を検討しましょう。
まとめ
WindowsCEのC++ライブラリをAndroidへ移植を検討する場合は、技術的な課題がたくさんあるので、1つずつ確認された方が良いでしょう。プラットフォームへの依存が少しでもあれば、移植が難しいケースが多いです。