見出し画像

Android Studioでndkを扱う(2)

はじめに

前回の投稿から時間がかなり経過し、今回書くネタや構想を忘れてしまいました。はじめてこの記事を開いた方は、下記の前回リンクからandroid ndkについて読んで頂ければと思います。

今回は、1回目の最後に「ndkライブラリのテスト用アプリを別モジュールとして作成し、結合しaarやlibsの話も記載します。」と記載していました。
とはいえ、githubリンクにはこれらの内容が全て取り込んでいますので、駄文は結構って方はソースを読んで理解頂ければと思います。

余談

Androidのアプリ開発においてndkで作る意味というのは何でしょうか?
人それぞれ個別の価値観を持っている はずです。それぞれの価値観で考えて頂ければ良いのですが、私個人としてはc/c++言語のネイティブコンパイラによる高速性は十分魅力的ですよね。それ以外には、OSへのアクセス性や膨大なOpen Sourceを参照できることなどが挙げられます。

欠点としては、どうしてもAndroid開発者はJava/Kotlinといった高級言語(人間にとって可読性が高い言語)であるため、それらに比して可読性が低いc/c++言語では開発スキルが高くなければなりません。
個人的には、c++も書けますけど多重継承を認めているため好きになれません。私はc++では一切多重継承はしません、というか途中から訳が分からなくなるので嫌なだけです。

そもそも、ndkを7年ぶりに本格的に手をだしたのは、諸事情があってAndroid R以降からipコマンドをJavaから叩けなくなる制限が入りました。
Android P以前は"/proc/net/arp"をアクセスすることがJavaからできています。Linuxユーザならご存知ですね。
Android Qはセキュリティ制限からアクセス禁止になったため、ipコマンドを実行することで取得できました。
Android Rからipコマンドへのアクセス禁止になったため、arp情報を取得することができなくなりました。そこで、stackoverflowではgetifaddrをndkで実行することで動作するという話を見つけ、実際にAndroidで実装したという経緯。
なお、Android SはBeta 1(対象はPixel 4以降)が先日リリースされたのですが、私はPixel 3aしか持っていないので未確認です。Pixel 6をGW特価でGoogleから発注しましたが、5末納品ってことらしく。

テストモジュールを作る

1.プロジェクト内で新規モジュールを作成する

File→New→New Moduleを選択します。

Create New Module

画面サンプル自体は、前回と同じ画面を流用しています。
ただ、テンプレート選択はjavawrapperはAndroid Libraryでしたらテスト用アプリなので、Phone&Tabletを選択します。
ここでは、Githubサンプルソースと同じmyapplicationをModule nameに設定したものとして話を進めます。変更した場合等は適宜読み替えてください。

Create New Module 2

ここではEmpty Activityを選んでおきます。ぶっちゃけNo Activityでも自力でActivityを作れるなら問題ありません。

Create New Module 3

最後にActivity名と画面レイアウトの名前を設定します。デフォルトでも構いません。実際、ちゃんとした開発をするなら、プロジェクト内での命名規約やコーディング規約を整える必要がありますね。
何でも構わないと言われても、実は暗黙のルールがあって違うとセンスが無いとか、未熟とか言われますので注意してください。

これでこのモジュールの実行環境を整えるとそのまま実行できます。

2.実行/デバッグ環境を準備する

最初にプロジェクト内に複数のモジュール(アプリケーションやライブラリ)が入っている場合、どれを指すのかIDE側(Android Studio)が分かりませんので、先程作ったmyapplication或いはその配下のファイルを選択しておいてください。
次に、RUN→Edit Configurations…を選択するか、ツールバーアイコンのrunボタン「▶」の左にあるリストボックスからEdit Configurations…を選択します。

Run/Debug Configurations

ここではappがひょっとするとあるのか、はたまた無いのかは今は覚えていません。
あった場合は面倒なので、左ペインからappを選択して「-」ボタンを押して削除しましょう。appが無ければ次に。

左ペインから「+」ボタンを押してAndroid Appを選択します。
上記Run/Debug Configurations画面のAppの名称がUnnamedになっていると思いますので、myapplicationに最上位のName欄に入れて変更します。
次に、Module欄でmyapplicationを選択できれば完了です。残りはデフォルト値で構いません。
ndkライブラリはまだ組み込んでいませんので、実行してもサンプルとして動作するのみです。
Androidアプリ初心者なら、ここで▶ボタンを押して実行してみるのも良いでしょう。

3.javawrapperモジュールの組み込み

File→Project Structureまたは、ツールバーアイコンのフォルダみたいなやつまたは、Ctrl+Alt+Shift+sのショートカットキーで起動できます。

Project Structure

画面上の通り、Dependencies(依存関係)ペインからmyapplicationを選択しDeclared Dependenciesの「+」ボタンを押します。画面はキャプチャできていませんが、3つの選択肢が出てきます。

  1. Library Dependency

  2. JAR/AAR Dependency

  3. Module Dependency

この中から「3. Module Dependency」を選択します。

Add Module Dependency

ここで、javawrapperが出てきます(未選択のプロジェクト内モジュールが表示されます)ので選択しOKボタンを押すことで組み込まれます。

Project Structure

この行為はUI操作でなくエディタ操作したい場合、myapplication内のbuild.gradleを開くと以下の行が見えるかと思います。
このような修正を手作業で行う方には蛇足でしたね。

implementation project(':javawrapper')
build.gradle全体

4.テンプレートソースの修正

テスト用アプリケーションにモジュールを組み込みましたが、ソースを書かないと動作しません。

ソース部のキャプチャ画面
package com.example.myapplication;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.widget.TextView;
import com.example.javawrapper.nativecall;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Native Call
        nativecall nc = new nativecall();
        // Example of a call to a native method
        TextView tv;
        tv = findViewById(R.id.sample_text);
        tv.setText(nc.getdata());
    }
}

ソース部分の内容、githubリンクもしています。
Native Callとコメントを記載している部分からが追加している部分です。

5.javawrapperのネイティブライブラリのコピー

最後になりましたが、javawrapperでビルドしたaarは組み込まれるのですが、前回の6.Nativeライブラリのビルドでビルドされたsoファイルは自動的には組み込まれません。実体は、javawrapperフォルダの直下にlibsフォルダがあり、そこに配置されています。

赤枠の部分にあるxxxx.so

Androidのネイティブビルドでは動作するCPU毎にビルドされます。(初期はarmeabi, MIPS, MIPS64までありました)
Javaは実行時にJITコンパイラで翻訳しながら動作するのですが、ネイティブは事前にビルド済でなければなりません。だから速いのですが。

では、どこにlibsファイルを配置すれば良いのでしょうか?

実際は、myapplication→src→main→jniLibsにlibsの中身をコピーすれば良いのです。ディレクトリ名はLのみ大文字で残り全て小文字で限定です。

この部分のgithubリンクを添付します。つまり、.soファイル(Unix系の共有ファイル)は変更があった場合のみ入れ替えることになります。

ではAndroidエミュレータで実行してみます。

6.実行

うまく動作すると以下の画面のように表示されます。

テスト実行画面

最後に

javawrapperでndkを中継する面倒なことをしたくないと言う方もいるかと思います。まぁ当然ではあるのですが、汎用的に作るならこの形式になるかと思います。

extern "C" JNIEXPORT jstring JNICALL
Java_com_example_javawrapper_nativecall_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    char hello[20] = "Hello from C++";
    return env->NewStringUTF(hello);
}

cppファイルの中身ですが、JNIインタフェース機構を利用してndkが動作します。このJNIインタフェースは悪用を避けるため、呼び出しJavaのパッケージ名およびメソッド名までを持っています。
そのため、別プロジェクトで使いたいとか、リファクタリングしたいといった要件では、cppソースを修正しビルドし直し、テスト実行しパスさせる必要があるのです。

故に、wrapperソースを経由させることにより、作成時には手間なのですがメンテナンス時に楽ができるのです。
ndkのテンプレートでは確かに動作するのですが、これでは複数名で担当するプロジェクトでは、一人のndk技術者のスキルの一部を開発担当者全員が持つ必要があるのです。
ライブラリ化という仕組み自体の考え方の原点ですね。
他プロジェクトのaarを組み込む場合、File→Import Moduleから行います。jniLibsの中身まではaarに含まれませんので、コピーして配置します。

ndkの作り方云々はこれで終了します。
次回が最後の予定です。getifaddrを公開します。


この記事が気に入ったらサポートをしてみませんか?