2010年12月02日

Android NDK でネイティブ CUI プログラムを書く!

はてなブックマークに登録

(2015年6月追記)
2014年10月に公開された Android 5.0 (Lollipop) 以降では PIE (Position Independent Executable) 以外のネイティブ実行形式がサポート外となったため注意が必要です。詳細は本ブログの次の記事を参照して下さい。

「Android で今後ネイティブ実行形式を扱う際に注意すべきこと」(2015年6月17日掲載)


今ならとても有利な条件で au の Android 端末を入手できることをネットで知り、先週の休みに地元の家電量販店へ足を運んでみました。 Android 1.6 搭載のこの「IS01」に今後 OS のバージョンアップサポートが適用されない旨の発表を聞いた時はお気の毒に・・と思っていましたが、極端に低いコストで実機を持てるのなら話は別です。とりあえずの実験用としていろいろ使えることでしょう。

そんなわけでこの何日間か Android SDK を勉強しながら Java のプログラムを書いたりしていたのですが、Android NDK を使えば ネイティブコードの JNI 用ライブラリだけではなく スタンドアロンの CUI プログラムも作成できることを知りました。

それはそれで面白いので Hello world! ではなくさっそく変なプログラムを C 言語で書いてみました。Android 端末上で次のように動作します。
・ 簡易 HTTP サーバとして振る舞い PC 上の web ブラウザと対話できる
・ フォームから指定された場所の地図を Android 端末上に表示する

# プログラム作成中の自問自答
Q:「もしかすると普通に PC で Google Maps を使う方が便利ではないか?」
A:「GPS の恩恵あり!入力の楽な PC キーボードを使えるのも良し!」
Q:「なら普通に SDK でそういうアプリを作ればよいのではないか?」
A:「Android の根っこは Linux!C でさくさく書くのが粋というものだ!」

粋かどうかはさておき興味のある方はご覧下さい。ソースコードも掲載します。

   ■ こんな風に使います
   ■ 処理内容
   ■ ソース
   ■ ビルド環境の作成手順
     A. Android 端末側の設定
     B. Windows 環境での設定
     C. Mac OS X 環境での設定
   ■ asvr のビルド~実機で動かす
   ■ 注意事項


■ こんな風に使います


  1. Android 端末でターミナルエミュレータアプリ (※1) を実行し localhost 上の「asvr」 (※2) を起動する
    (※1: マーケットで公開されている無償のものを使うといいでしょう。こういうのとか)
    (※2: 後述の手順でビルドし端末へ転送した実行形式のバイナリです)
  2. asvr 起動時に表示される IP アドレス+ポートに PC 上のブラウザから http://[アドレス]:[ポート番号]/ の要領でアクセスする
  3. ブラウザ上のフォームに地名・キーワード等を入力して submit すると Android 端末上で地図検索が走り結果が表示される
  4. asvr の終了は Ctrl+C (※3)
    (※3: Ctrl キーのアサインはターミナルアプリ側の仕様によります)


■ 処理内容


  • asvr は TCP/IP サーバプロセスとして動作します。デフォルトで 8888 番ポートを listen し、ネットワーククライアントからのリクエストに HTTP レスポンスを返します。
  • asvr は HTTP クライアントから受け取ったパラメータ文字列を地図表示用の インテント として編集し、それを Android の am コマンド経由で発行します。 (インテントに関する詳しい記事)


■ ソース

asvr/jni/Android.mk
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE    := asvr
LOCAL_SRC_FILES := asvr.c
include $(BUILD_EXECUTABLE)
asvr/jni/asvr.c
/*
 * asvr.c
 * 
 * Copyright(c) 2010 KLab Inc.
 */
 
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <net/if.h>
 
#define DEFAULT_PORT 8888
 
#define RES_HEAD_FMT \
   "HTTP/1.0 200 OK\r\n" \
   "Content-Type: text/html\r\n" \
   "Connection: close\r\n" \
   "Content-Length: %d\r\n\r\n"
 
#define RES_BODY_FMT \
   "<html>" \
   "<meta http-equiv='Content-Type' content='text/html; charset=UTF-8' />" \
   "<head></head>" \
   "<body>" \
   "%s<br>" \
   "<form action='/' method='GET'>" \
   "<input type='text' name='q' maxlength='20' value=''>" \
   "<input type='submit'>" \
   "</form>" \
   "</body>" \
   "</html>" 
 
#define REQ_GETSTR "GET /?q="
#define REQ_GETSTR_LEN 8
 
// am コマンドで地図表示インテントを発行
int pushIntent(const char *param, char *resbuf, int buflen) {
  FILE *fp;
  char buf[512];
  int len;
  if (!param || (len = strlen(param)) <= 0 ||
    !resbuf || buflen <= 0) {
    return 0;
  }
  strcpy(buf, "am start -a android.intent.action.VIEW geo:0,0?q=");
  if (len >= sizeof(buf) - strlen(buf)) { // 長すぎ
    return 0;
  }
  strcat(buf, param);
  fp = popen(buf, "r");
  if (!fp) {
    return 0;
  }
  len = fread(resbuf, 1, buflen-1, fp);
  resbuf[len] = '\0';
  pclose(fp);
  return strlen(resbuf);
}
 
// パラメータチェック
int validate(const char *str, int len) {
  int i;
  if (!str || len <= 0) {
    return 0; // NG
  }
  for (i = 0; i < len; i++) {
    char c = str[i];
    if (c != '%' && c != '+' &&
      !(c >= '0' && c <= '9') &&
      !(c >= 'a' && c <= 'z') &&
      !(c >= 'A' && c <= 'Z')) {
      return 0; // NG
    }
  }
  return 1;
}
 
// 受送信スレッド
void *thRecvSend(void *p) {
  int sd = (int)p;
  int len;
  char *pt1, *pt2;
  char buf[2048];
  char res_head[256];
  char msg[128];
  char q[128];
 
  len = recv(sd, buf, sizeof(buf)-1, 0);
  if (len <= 0) {
    goto done;
  }
  buf[len] = '\0';
  //printf("req=[%s]\n", buf);
 
  if (strncmp(buf, REQ_GETSTR, REQ_GETSTR_LEN) != 0) {
    // フォームのみを返す
    snprintf(buf, sizeof(buf), RES_BODY_FMT, "");
  } else {
    pt1 = buf + REQ_GETSTR_LEN;
    pt2 = strstr(pt1, " HTTP/");
    if (pt2) {
      *pt2 = '\0';
      snprintf(q, sizeof(q), "%s", pt1);
      if (!validate(q, strlen(q))) {
        pt2 = NULL;
      } else {
        // インテント発行
        if (pushIntent(q, msg, sizeof(msg)) > 0) {
          snprintf(buf, sizeof(buf), RES_BODY_FMT, msg);
        } else {
          pt2 = NULL;
        }
      }
    }
    if (!pt2) { // NG
      snprintf(buf, sizeof(buf), RES_BODY_FMT, "???");
    }
  }
  snprintf(res_head, sizeof(res_head), RES_HEAD_FMT, strlen(buf));
  send(sd, res_head, strlen(res_head), 0);
  send(sd, buf, strlen(buf), 0);
  //printf("res=[%s%s]\n", res_head, buf);
done:
  shutdown(sd, 2);
  close(sd);
  return NULL;
}
 
// サーバアドレス・ポートを表示
void showServerInfo(int sd, short port) {
  int i, num;
  struct ifreq ifr, ifrs[8];
  struct ifconf ifc;
  struct sockaddr_in *sin;
 
  // ifc_len は in,out
  ifc.ifc_len = sizeof(ifrs);
  ifc.ifc_ifcu.ifcu_buf = (char*)ifrs;
  if (ioctl(sd, SIOCGIFCONF, &ifc) == -1) {
    printf("ioctl SIOCGIFCONF: errno=%d\n", errno);
    return;
  }
  num = ifc.ifc_len / sizeof(ifr);
  for (i = 0; i < num; i++) {
    ifr.ifr_addr.sa_family = AF_INET;
    strcpy(ifr.ifr_name, ifrs[i].ifr_name);
    if (ioctl(sd, SIOCGIFADDR, &ifr) == 0) {
      sin = (struct sockaddr_in *)&ifr.ifr_addr;
      printf("addr=%s port=%d (%s)\n", 
        inet_ntoa(sin->sin_addr), port, ifr.ifr_name);
    }
  }
}
 
// シグナルハンドラ
void handler(int sig) {
  if (sig == SIGPIPE) {
    signal(SIGPIPE, handler); // 再設定
  } else if (sig == SIGTERM || sig == SIGINT) {
    puts("bye");
    exit(1);
  }
}
 
// 主処理
int server(int ac, char *av[]) {
  int sock, sd, len, err, i = 1;
  pthread_t thread;
  struct sockaddr_in my_sin;
  struct sockaddr_in peer_sin;
  short port = 0;
 
  // 待機ポート番号
  if (ac > 1) {
    port = atoi(av[1]);
  }
  if (port == 0) {
    port = DEFAULT_PORT;
  }
  signal(SIGPIPE, handler);
  signal(SIGTERM, handler);
  signal(SIGINT,  handler);
 
  if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
    printf("socket: errno=%d\n", errno);
    goto done;
  }
  memset(&my_sin, 0, sizeof(my_sin));
  my_sin.sin_family = AF_INET;
  my_sin.sin_port = htons(port);
  my_sin.sin_addr.s_addr = INADDR_ANY;
  setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (char*)&i, sizeof(i));
 
  if (bind(sock, (struct sockaddr*)&my_sin, sizeof(my_sin)) < 0) {
    printf("bind: errno=%d\n", errno);
    goto done;
  }
  if (listen(sock, 10) < 0) {
    printf("listen: errno=%d\n", errno);
    goto done;
  }
 
  // サーバアドレスとポート番号を表示
  showServerInfo(sock, port);
 
  while (1) {
    len = sizeof(peer_sin);
    sd = accept(sock, (struct sockaddr *)&peer_sin, &len);
    if (sd == -1) {
      printf("accept: errno=%d\n", errno);
    } else {
      err = pthread_create(&thread, NULL, thRecvSend, (void*)sd);
      if (err != 0) {
        printf("pthread_create: err=%d\n", err);
      }
    }
  }
done:
  if (sock != -1) {
    close(sock);
  }
  return 0;
}
 
// main
int main(int ac, char *av[]) {
  setbuf(stdout, NULL);
  return server(ac, av);
}


■ ビルド環境の作成手順

ここには、NDK を利用して Windows, Mac OS X 上で Android 用のバイナリをクロスコンパイルし、それを実機へ転送するための必要最低限の環境を作成する方法のみを記しています。すでに万全の Android 開発環境を作成ずみの方はスキップして下さい。
A. Android 端末側の設定
Android 端末側で「USBデバッグ」を有効に設定しておきます。([設定]-[アプリケーション]-[開発]-[USBデバッグ] ←全機種共通?) この設定により USB ケーブルで接続した PC から「adb コマンド」(*1) 経由で端末へのアクセスが可能となります。
(*1:adb =「Android Debug Bridge」Android SDK に含まれる開発用ユーティリティ)

B. Windows 環境での設定
1. Android SDK のインストール
※ 2010/12/02 現在の最新バージョンは「android-sdk_r07」
http://developer.android.com/sdk/index.html から Windows 用 SDK アーカイブをダウンロードし内容を「C:\android-sdk-windows」等に展開。システムの[プロパティ]-[詳細設定]-[環境変数]パネルから「システム環境変数」の PATH に「C:\android-sdk-windows\tools」を追加します。これにより前述の adb コマンドにパスが通ります。

2. USBドライバ・ADBドライバのインストール
Android 端末のメーカーまたはベンダが機種ごとにドライバを提供しています。
インストール方法や初回の接続手順は機種ごとの情報を参照して下さい。
※IS01 用のドライバと説明はシャープ社サイトのこのページにあります。
インストールが終わったら、端末を USB 接続した状態で Windows の DOS コンソールから「adb devices」コマンドを実行することで疎通を確認できます。実行結果として何らかのエントリが表示されればドライバは正常に動作しています。

3. Android NDK のインストール
※ 2010/12/02 現在の最新バージョンは「android-ndk-r4b」
http://developer.android.com/sdk/ndk/index.html から Windows 用 NDK アーカイブをダウンロードし内容を「c:\android-ndk」等に展開します。

4. Cygwin 環境の作成
Cygwin は Windows 用に移植された GNU 開発ツールのセットです。NDK を使ってバイナリをビルドするのに必要です。 公式サイトから setup.exe をダウンロード・実行してデフォルトのままウィザードを進め、「Choose A Download Site」パネルで国内のサイトを指定して下さい。
「Select Packages」パネルで、「Devel」カテゴリ下の「gcc-core」「gcc-g++」「make」のインストールを指定します。 また、UNIX 環境でのテキスト編集に慣れている人はあわせて「Editor」カテゴリでお好みのエディタのインストールを指定しておけば後で便利でしょう。
指定をすませて継続すると依存関係に準じて必要なパッケージがインストールされます。 Cygwin のインストールが終わったらスタートメニューから「Cygwin bash shell」を起動し、ホームディレクトリ下の .bashrc の末尾に下記を加えて一旦シェルを終了して下さい。
export ANDROID_NDK_ROOT=/cygdrive/c/android-ndk
export PATH=$PATH:$ANDROID_NDK_ROOT
前者は Cygwin シェル環境下でのビルド中に NDK ディレクトリを特定するために参照されます。後者はビルド用の android-ndk/ndk-build スクリプトにパスを通すことが目的です。

※後述のビルド手順の説明では、便宜上「Cygwin bash shell」のことを単に「シェル」と呼び、「Cygwin bash shell」起動直後の Cygwin の世界でのホームディレクトリを単に「ホームディレクトリ」と呼びます。このディレクトリの実体はデフォルトで Windows 上の「C:\cygwin\home\[ユーザ名]\」です。

C. Mac OS X 環境での設定
1. Android SDK のインストール
※ 2010/12/02 現在の最新バージョンは「android-sdk_r07」
http://developer.android.com/sdk/index.html から Mac 用 SDK アーカイブをダウンロードし内容を「[ホームディレクトトリ]/android/android-sdk-mac_x86」等に展開します。

2. Android NDK のインストール
※ 2010/12/02 現在の最新バージョンは「android-ndk-r4b」
http://developer.android.com/sdk/ndk/index.html から Mac 用 NDK アーカイブをダウンロードし内容を「[ホームディレクトトリ]/android/android-ndk」等に展開します。

3. 環境変数の設定
[Macintosh HD]-[アプリケーション]-[ユーティリティ]-[ターミナル]を実行してシェルを起動します。ホームディレクトリ下の .bash_profile に下記要領の行を追加し環境へ反映させて下さい。
export PATH=$PATH:[ホームディレクトトリ]/android/android-sdk-mac_x86/tools
export ANDROID_NDK_ROOT=[ホームディレクトトリ]/android/android-ndk
export PATH=$PATH:$ANDROID_NDK_ROOT
一行めは前述の adb コマンドにパスを通すことが目的です。二行めはビルド処理が NDK ディレクトリを特定するためのもので、三行めはビルド用の android-ndk/ndk-build スクリプトにパスを通すことが目的です。

4. Android 端末を USB ケーブルで接続
Mac の場合は別途ドライバをインストールする必要はありません。端末を USB 接続した状態でシェルから「adb devices」コマンドを実行することで疎通を確認できます。実行結果として何らかのエントリが表示されれば OK です。ただし、疎通のために設定の編集が必要なケースもあるらしいので NG の場合は機種名でググって情報を探して下さい。


■ asvr のビルド~実機で動かす


1. asvr.c と Android.mk の配置
ホームディレクトリ下に適当なディレクトリ(例:[ホームディレクトリ]/bld/)を用意し、bld/asvr, bld/asvr/jni の各ディレクトリを作成して下さい。 bld/asvr/jni/ 下に、asvr.c と Android.mk のふたつのファイルを配置します。

2. ビルド
シェルを起動し以下を実行します。
$ cd bld/asvr
$ ndk-build -B
ビルドが正常に終了すれば bld/asvr/libs/armeabi/ 下に asvr の実行形式が生成されます。

3. 実機への転送
ビルドずみの asvr を adb を使って Android 端末へ転送します。ここでは転送先のディレクトリを /data/local/tmp としています。
$ adb push libs/armeabi/asvr /data/local/tmp

4. 実行属性設定と実行確認
adb shell コマンドで Android 端末にコンソール接続し asvr に実行属性を付与します
$ adb shell
(以下 adb shell 内)
  $ cd /data/local/tmp
  $ chmod 755 asvr
あわせて adb shell 上で asvr の起動をテストしてみましょう。
  $ ./asvr
  addr=127.0.0.1 port=8888 (lo)
  addr=XXX.XXX.XXX.XXX port=8888 (XXXX)
上記要領の表示になれば OK です。CTRL+C で asvr を終了し adb shell を exit します。
  ^C bye
  $ exit
$

5. Android 端末上で動かしてみる
以上で実機へのモジュールの配置は完了です。最初の項の手順に添って実機上で asvr を起動し、PC 上のブラウザからアクセスを試して下さい。


■ 注意事項


ご覧の通りこれは実験のためのごく簡単なプログラムです。手元にある一台の Android 端末以外ではまったく動作確認を行っていませんが、「ためしに動かしてみよう!」という場合は LAN 環境での使用に留めて下さい。また、使い方にかかわらず万一何か困ったことになっても筆者も弊社も一切責任を持てません。実験は楽しく自己責任で :-)
(tanabe)
klab_gijutsu2 at 21:22│Comments(6)TrackBack(0)Android | win

トラックバックURL

この記事へのコメント

1. Posted by 安藤炉囲土   2010年12月02日 22:30
こんにちは

Cloud-to-Device Messaging APIの簡易版ですね

確かに粋です
2. Posted by tanabe   2010年12月03日 09:29
安藤炉囲土さん、コメントをありがとうございます。
ヒントはまさに C2DM でした(笑)。お褒めの言葉を頂き恐縮です。
3. Posted by のり   2011年01月04日 14:49
こんにちは。

Android x86 Floyoで試してみたのですが
adb shell 上で not executable: magic 7F45
となるのはコンパイルに失敗してるのでしょうか?
4. Posted by tanabe   2011年01月04日 15:46
のりさん、コメントをありがとうございます。
ARM アーキテクチャ用に NDK でクロスコンパイルしたバイナリを x86 ベースの Android x86 環境で動かすことはできませんね。
ちなみに、現行の NDK では x86 用バイナリの生成はサポートされていませんが、公式サイトに「Future releases of the NDK will also support」として関連する記事があります。ご参照下さい。
http://developer.android.com/sdk/ndk/overview.html
5. Posted by のり   2011年01月04日 16:23
通りすがりの質問に早速、ご丁寧にアドバイス頂き
ありがとうございます。

確かにCPUのアーキテクチャが違いましたね。
気がつかないとは・・・

関連記事も確認し、実機でも試してみます。

6. Posted by tanabe   2011年01月04日 16:39
のりさん、コメントをありがとうございます。
いえいえ、技術力の高い方でもぽつんと盲点となりそうな話題ですね。今後とも本ブログを宜しくお願い致します。

この記事にコメントする

名前:
URL:
  情報を記憶: 評価: 顔   
 
 
 
Blog内検索
Archives
このブログについて
DSASとは、KLab が構築し運用しているコンテンツサービス用のLinuxベースのインフラです。現在5ヶ所のデータセンタにて構築し、運用していますが、我々はDSASをより使いやすく、より安全に、そしてより省力で運用できることを目指して、日々改良に勤しんでいます。
このブログでは、そんな DSAS で使っている技術の紹介や、実験してみた結果の報告、トラブルに巻き込まれた時の経験談など、広く深く、色々な話題を織りまぜて紹介していきたいと思います。
最新コメント
「最新トラックバック」は提供を終了しました。