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 FreeBSD(VM上の 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で実行したときと結果が違っています。面白いですね。
ちなみに、
のどれも同じ結果になりました。