アーキテクチャをスマートに。

株式会社ネオジニア代表。ITアーキテクトとしてのお仕事や考えていることなどをたまに綴っています。(記事の内容は個人の見解に基づくものであり、所属組織を代表するものではありません)

Android NDK の C++ 開発でハマる(2)

続きです。

答え

要するに、

 1l << cardId

の演算が32bitになってるわけです。
当然、cardId が 32 以上になると桁あふれし、想定外の動きとなってしまいます。

そうです。

Androidは32bit、PC(Ubuntu)は64bit、という違いです。

今回の例では、この「1l」という書き方がまずい、というわけですね。
サフィックス「l」は long 型を意味しているので、32bitか64bitかは環境依存です。
ここがC言語の怖いところです。
変数型は、stdint.h に定義されている int16_t とか uint64_t とかを使えば、環境依存せずにビット幅を決められますが、リテラルは無理です。
→これ間違いです。リテラルに対応するためのマクロが stdint.h にあるようです。INT64_C(1) と書けば 64bit になるようです。

試しに

確認のため、こんなテストプログラムを書いてみました。

プログラム check64.c

#include <stdio.h>
#include <stdint.h>

#define LOGI(...) printf(__VA_ARGS__)

void check()
{
 // 型サイズのチェック
 LOGI("sizeof short     = %d\n", (int) sizeof (short));
 LOGI("sizeof int       = %d\n", (int) sizeof (int));
 LOGI("sizeof long      = %d\n", (int) sizeof (long));
 LOGI("sizeof long long = %d\n", (int) sizeof (long long));
 LOGI("sizeof uint64_t  = %d\n", (int) sizeof (uint64_t));
 LOGI("sizeof size_t    = %d\n", (int) sizeof (size_t));
 LOGI("sizeof void*     = %d\n", (int) sizeof (void*));

 // リテラルサフィックスのチェック
 int x = 33;
 uint64_t a = 1 << x;    // 33と直接書くと警告してくれる
 uint64_t b = 1l << x;
 uint64_t c = 1ll << 33;
 uint64_t d = 1; d <<= 33;

 LOGI("a = 0x%08x-%08x\n", (uint32_t)(a>>32), (uint32_t)a);
 LOGI("b = 0x%08x-%08x\n", (uint32_t)(b>>32), (uint32_t)b);
 LOGI("c = 0x%08x-%08x\n", (uint32_t)(c>>32), (uint32_t)c);
 LOGI("d = 0x%08x-%08x\n", (uint32_t)(d>>32), (uint32_t)d);
}

int main(void)
{
 check();
 return 0;
}


これを 64bit PC(Ubuntu Linux 11.10 x86_64) で実行した結果。

$ uname -a
Linux ThinkPad-X220 3.0.0-15-generic #25-Ubuntu SMP Mon Jan 2 17:44:42 UTC
 2012 x86_64 x86_64 x86_64 GNU/Linux
$ gcc --version
gcc (Ubuntu/Linaro 4.6.1-9ubuntu3) 4.6.1
Copyright (C) 2011 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

$ 
$ cc check64.c && ./a.out
sizeof short     = 2
sizeof int       = 4
sizeof long      = 8
sizeof long long = 8
sizeof uint64_t  = 8
sizeof size_t    = 8
sizeof void*     = 8
a = 0x00000000-00000002
b = 0x00000002-00000000
c = 0x00000002-00000000
d = 0x00000002-00000000
$
  • aは32bitで演算されているので、思ったとおりの結果になっていません。(33 % 32 で1ビットしかシフトされていない)
  • bは思った通りの結果になっており、64bitで演算されてます。sizeof (long) が 8 なので、long型は8バイト、「1l」も64bitになるわけです。
  • cは「1ll」なので、long long、つまりこれも64bitですね。
  • dはリテラルの演算ではなく、一旦64bit変数に入れてから、変数に対して演算しています。実はこれが一番安全で確実です。

※64bit Windows では、VC++を使うとlong型は 32bitになるようです。LLP64データモデル(long long とポインタだけが64bit)と言うそうです。手元に環境がないので試せていませんが。
ポインタが64bitなので、DWORD にポインタを入れたりしてる場合は完全にアウトですね。昔よくやってた。。。上記Linuxの場合はLP64データモデル(long以上とポインタが64bit)となります。
http://ja.wikipedia.org/wiki/64%E3%83%93%E3%83%83%E3%83%88


32bit PCで実行すると

同じプログラムを 32bit FreeBSDVM上の FreeBSD 7.0 i386)でもコンパイルして実行してみました。
Windowsは手元に環境がないので試していませんが、32bit Windows なら同じ結果になるはずです。

> uname -a
FreeBSD eclair 7.0-RELEASE FreeBSD 7.0-RELEASE #0: Sun Feb 24 19:59:52 UTC
 2008     root@logan.cse.buffalo.edu:/usr/obj/usr/src/sys/GENERIC  i386
> gcc --version
gcc (GCC) 4.2.1 20070719  [FreeBSD]
Copyright (C) 2007 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

> cc test64.c 
> ./a.out 
sizeof short     = 2
sizeof int       = 4
sizeof long      = 4
sizeof long long = 8
sizeof uint64_t  = 8
sizeof size_t    = 4
sizeof void*     = 4
a = 0x00000000-00000002
b = 0x00000000-00000002
c = 0x00000002-00000000
d = 0x00000002-00000000
> 
  • sizeof (long) が 4になっています。
  • aはまだしも、bが思った通りの結果になっていません。
  • cはOKです。sizeof (long long) が 8なので。
  • dも当然OK。

Android では?

最後に、NDKでビルドしてAndroidで実行してみました。
JNIインタフェースを書いてJavaアプリから呼び出します。
アプリは、Eclipseで「新規 Android Project」を作っただけです。
プログラムはこんな感じ。

Test64Activity.java

package com.lobins.test.test1;

import android.app.Activity;
import android.os.Bundle;

public class Test64Activity extends Activity {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        checkTypes();  // C側の check() を呼び出す
    }

    static {
    	System.loadLibrary("check64");
    }
    
    private native int checkTypes();
}

jni/test64.c

#include <stdio.h>
#include <stdint.h>

#include <jni.h>
#include <android/log.h>

#define LOGI(...) ((void)__android_log_print(ANDROID_LOG_INFO, "checkTypes", __VA_ARGS__))

void check()  // この関数はまったく同じ内容
{
 // 型サイズのチェック
 LOGI("sizeof short     = %d\n", (int) sizeof (short));
 LOGI("sizeof int       = %d\n", (int) sizeof (int));
 LOGI("sizeof long      = %d\n", (int) sizeof (long));
 LOGI("sizeof long long = %d\n", (int) sizeof (long long));
 LOGI("sizeof uint64_t  = %d\n", (int) sizeof (uint64_t));
 LOGI("sizeof size_t    = %d\n", (int) sizeof (size_t));
 LOGI("sizeof void*     = %d\n", (int) sizeof (void*));

 // リテラルサフィックスのチェック
 int x = 33;
 uint64_t a = 1 << x;    // 33と直接書くと警告してくれる
 uint64_t b = 1l << x;
 uint64_t c = 1ll << 33;
 uint64_t d = 1; d <<= 33;

 LOGI("a = 0x%08x-%08x\n", (uint32_t)(a>>32), (uint32_t)a);
 LOGI("b = 0x%08x-%08x\n", (uint32_t)(b>>32), (uint32_t)b);
 LOGI("c = 0x%08x-%08x\n", (uint32_t)(c>>32), (uint32_t)c);
 LOGI("d = 0x%08x-%08x\n", (uint32_t)(d>>32), (uint32_t)d);
}

// JNI インタフェース
JNIEXPORT jint Java_com_lobins_test_test1_Test64Activity_checkTypes(JNIEnv* env, jobject thiz)
{
 check();
}

実行結果(LogCatより)

03-09 14:31:06.322: I/checkTypes(4129): sizeof short     = 2
03-09 14:31:06.322: I/checkTypes(4129): sizeof int       = 4
03-09 14:31:06.322: I/checkTypes(4129): sizeof long      = 4
03-09 14:31:06.322: I/checkTypes(4129): sizeof long long = 8
03-09 14:31:06.322: I/checkTypes(4129): sizeof uint64_t  = 8
03-09 14:31:06.322: I/checkTypes(4129): sizeof size_t    = 4
03-09 14:31:06.322: I/checkTypes(4129): sizeof void*     = 4
03-09 14:31:06.322: I/checkTypes(4129): a = 0x00000000-00000000
03-09 14:31:06.322: I/checkTypes(4129): b = 0x00000000-00000000
03-09 14:31:06.322: I/checkTypes(4129): c = 0x00000002-00000000
03-09 14:31:06.322: I/checkTypes(4129): d = 0x00000002-00000000
  • やはり sizeof (long) が4なので、long型は 4バイト、32bitです。
  • b がゼロになっています。33 % 32 されるのではなく、本当に33ビットシフトしているようです。Intel と ARM でCPUアーキテクチャが違うので、32bitPCで実行したときと結果が違っています。面白いですね。

ちなみに、

のどれも同じ結果になりました。

まとめ

移植性を考慮したプログラムを書くならば、プラットフォーム依存なAPIを使用しないだけでなく、データ型のサイズにも注意が必要です。
変数型だけでなくリテラルにも注意が必要です。

なかなか大変なことです。

こういう環境依存な事柄をすべて吸収し、仮想マシン上で実行するという発想、やっぱりJavaってすごいですね。