strlen と sizeof にまつわる不具合

以前に Mac 環境で C を使う場合、バイナリバイト列などのデータは NSData に任せた方がいいということは述べた。

いうまでもなく、C のデータの取り扱いの煩雑さは負担と考えているからだ。

そうはいっても、 既に(Objective ではない素の)C で各種データの定義が完了しているような場合、そういうわけにもいかない。例えば C で書かれた汎用ライブラリの機能に大きく依存しているようなプロジェクトではどうしてもどうなってしまう。

ここら辺、どの部分を NSData (というか cocoa の各種フレームワーク)に割り振り、どの部分を C で押し通すのかは、センスの出るところだと思う。

ところで、C 言語に関しては、巷の C 談義のようなものだけではうまくいかないと感じている。

ポインタの話は C を語る上で重要だが、ポインタだけ知っていても実用的なコードが書けるとは思えない。

ところで、この前、さるプログラムに不具合が出て、原因を調べていて気がついたのだが、以下のコードを実行した場合、結果はどうなると思います?

uint8_t AP1[] = {
        0x00, 0x20, 0x00, 0x88, 0x04
    };
printf("strlen(AP1): %d\n", strlen(AP1));
printf("sizeof(AP1): %d\n", sizeof(AP1));

この部分だけを取り出すとわかりやすいと思うが、結果は

strlen(AP1): 0
sizeof(AP1): 5

となる。

「終端文字 0x00 が先頭にあるのだから、文字列の長さを求める strlen では 0。使われているメモリのバイト数を求める sizeof では 5。当たり前じゃないか」と言われるかもしれないが、実際の(不具合のあった)コードはこんなにわかりやすくはない。

具体的には AP1 の末尾にバイト列を追加してバイト列を生成するためのメモリ確保の段階で

malloc(strlen(AP1) + strlen(hoge))

とやっていたんだな。

実は、これらコードを含むプログラムは USB を制御するコマンドで AP1[5] もそのコマンドの一つだった。

実際にデバイスを制御するわけだから、コマンドといえどもバイナリデータであってもおかしくはないのだが、そこら辺の事情をわからず「コマンド」と言われれば文字列を連想するのが普通ではなかろうか。

で、文字列と思いこんでこのような実装になってしまったと。

この手の不具合が発見しにくいのは、0 を含まない「コマンド」の場合は正しく動作してしまうからだ。

なお、バイト列とバイト列を結合させたい場合には

   uint8_t cmd[sizeof(AP1)+sizeof(hoge)];
    memcpy(cmd, AP1, sizeof(AP1));
    memcpy(cmd+sizeof(AP1), hoge, sizeof(hoge));

出力させる時は

for (int i = 0; i < sizeof(cmd); i++) {
            printf("%02x", cmd[i]);
    }

と書くとわかりやすいでしょうか。

 

arm Mac で C/C++ ライブラリを x86_64 向けにビルドする

すっかり arm 環境に移行した感のある Mac 界隈だが、なんらかの事情で Intel Mac 向けにアプリなどをビルドしたいという場合がある(案件だとね)。

アプリ自体は Xcode 上での操作でなんとかなると思うのだが、厄介なのはライブラリの類。

使いたい C/C++ のライブラリを M1/M2 Mac 上で x86_64 向けにビルドする必要が出てくる。
いわゆるクロスコンパイルというやつだ。

iOS の場合、simulator が x86_64 でしか動かないので、この手の情報はけっこうあったのだが、 MacOS 向けの情報は皆無だった。

試行錯誤したところ configure 時に以下のようなにコンパイルオプションを与えると、クロスコンパイルできるようだ。

./configure --prefix=/dir/hoge x86_64-apple-darwin64 CFLAGS='-arch x86_64 -O2 -g'

あとは make, make install でいけるっぽい。

–build オプションを使えとか、それっぽい情報はあるにはあった。
が、実行しても惜しいところまではいくんだが、なぜか最終的にはうまくいかなかった。

 

Xcode -aggeragate target-

ところで、であるが、アイキャッチの赤い三重丸のようなやつの正式名称を知っているだろうか?

aggeregate target

どことなく射的の的を連想させるアイコン(ターゲット)は aggeregate というらしい。

英単語としての発音は「アーグリゲイト」、一般的な意味は「集約する、合計する」だ。

日本語の情報はほとんどなく

ライブラリ・SDKにSwiftLintを導入するベストプラクティス

でかろうじて触れられているのみ。

aggregate target を生成するのもややクセがあって、

File -> New -> Target…

として(↓)、

ダイアログを表示させ iOS や macOS ではなく other の方を選ぶ。

これは調べないと分かりませんね。

参考

その他、Xcode やビルドシステムについて。

・cmake 単体で iOS のプロジェクトを取り扱う。

cmakeを使ってxcodeを使わずにiosアプリを作成する

・cmake で Xcode のプロジェクトファイルを生成する。

CMakeとIDEを連携させる

help によれば cmake で framework を直接生成できるらしいのだが、試したことはない。
(→結局、試しました

余談 -External Build System-

ところで、アーグリゲイトなターゲットを作成する際、こういう画面が出てくる。

気になるのは External Build Sysytem だと思うが、イマイチ使い方がわからない。

デフォルトだと make を使うように設定されているのだが、aggregate ターゲットでもいけるはずだが???

余裕があったら、調べます。

余談 Xcode での C++ 標準ライブラリのヘッダーファイルの置き場所

Xcode14 では

/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1

です。

よく移動になるみたいですね。

標準ライブラリのヘッダーファイルが見つからない場合は、上のフォルダを header search paths に設定する。

 

C/C++ 覚え書き -ポインタ-

やはり「C/C++ は難しい」という声をよく聞く。

実際、最近、twitter でこんなやりとりがあった。

ワイも極めるレベルまではいってないが、まあ困らない程度には使える。
古今東西の C で書かれたライブラリを使うために悪戦苦闘しているうちに何となく覚えてしまった。

C/C++ の習得を難しくしている原因の一つは、ポインタだろう。

ポインタの基本

twitter でも書いたが、ポインタの簡単なサンプルを上げておくとこんな感じか。

//test.c
#include <stdio.h>

int main(){

    //int *a;
    int* a;

    int b=100;

    a = &b;

    printf("%d", *a);

}

これをコンパイルする。素直にコンパイルするなら、こんな感じか。

gcc -o test test.c

これで test という実行可能なファイルができる。
実行させると、もちろん

100

と表示される。

わざわざ

int *a

も書いておいたのは、こう書いても結果は変わらないから。
ただ、こう書くと初学者の理解はがこんと悪くなるという印象を持っている。

なんで、ここでわからなくなるかといえば、おそらく、宣言時の *a と最後の printf で( a で差し示されている)実際の数値を表示させる *a を混同してしまうからだと思う。

ポインタ変数といえども変数には変わりなく、宣言が必要だ。もちろん、型も必要。

上の場合は、int 型のポインタ変数 a を宣言したいのだから、int* と書いた方が(少なくとも初学者にとっては)混乱は減ると思う。
あくまで変数としては a なのだから。

通常の変数と違うのは、ここにアドレスが格納されるという特殊性だろう。

a 自体はアドレスなのだから、その(アドレスで示されている)実体を知りたければ何らかの操作が必要で、シンプルに * をつける。
* は宣言時にも使われるので、両者を混同してしまうのだろう。

次の難所 -文字列と配列-

ポインタの概念が分かってもそれだけでは使えないと思う。

これはおそらく C の配列や文字列の扱いのせいでしょう。
取り扱いというか約束事ね。

配列

上のようなプリミティブな数値型の場合、ポインタを使いたければ * を使えばよかったが、配列も同じようにするとうまくいかない。

つまり

int[] *a;

という表現はしない
というかする必要がない。

というのは、C では、配列 array[] などと書いた場合、array つまり配列変数名自体はその先頭要素へのポインタになっているからだ。

サンプルコードを示すと以下のようになる。

#include <stdio.h>

int main()
{
    //int[] *p;
    
    int array[]={1,2};
    
    printf("%d",*array);
    
    return(0);

}

結果は 1 。
int[] *p という宣言は、エラーになる。

ところで、こういう書き方は、近年の配列に要素数が簡単に追加できたり型推論してくれたりする言語に慣れた人からすると、奇異に映るようだ。
奇異というか不安に近い感情だろうか。

array に要素を追加したい場合、どうするんですか?と。

安心して欲しい。C では、原則、要素数の追加はできない
(C での可変長配列に関してはここらあたりの記事参照)

なお、この場合、配列 array の要素数を知りたければ、以下のようななんとも面倒くさい書き方になる。
こちらのオンライン C コンパイラーサイトを使用)

array 自体のメモリサイズを sizeof で求めて、それを要素のバイト数で割って、ようやく要素数を知ることができる。

このコードにしても unsigned 絡みでワーニング出てますけどね。

ついでで言っておくと C/C++ ではファイルサイズを調べるときも(直接これを調べるような関数のようなものは提供されていないので)同様に若干手間のかかるアプローチを取る必要がある。

このツィなど参照。

大きさを調べるときには注意が必要

なお、(初学者は絶対混乱すると思うが)array[] という書き方と *array という書き方は C コード内で厳密にはまったく同じ挙動をするわけではない。

よく知られた例としては、sizeof(*array) とするとポインタの大きさを返すのみで、array 自体の大きさを返しているわけではない

バグの原因となる。

ちょっと実用的なサンプル -バイナリファイルを 2byte 毎に読み込む-

ここら辺の仕様はややこしいところだが、実務的には便利なときがある。

あるバイナリファイルがあった時、これをバイト配列ではなく、2byte や 4byte の数値配列として読み込みたいということがよくある。
C++ になってしまうが、以下のようなコードはよくお目にかかる。

    const char* fname="(PathTo)/sample.raw";
    std::ifstream ifs(fname, std::ios::binary);
    ifs.seekg(0,std::ios::end);
    uint64_t size = ifs.tellg();
    ifs.seekg(0);

    uint16_t *data16 = new uint16_t[size/2];
    ifs.read((char*)data16,size);

このコードのキモは

ifs.read((char*)data16,size)

だ。
ifstream::read() は、仕様的にはバイト配列しか読み込めないが、2byte 配列 data16 のポインタをバイト配列のポインタでキャストすることで 2byte の配列として読み込んでくれる。

いったん、バイト配列として読み込んだ後、2byte の配列にマッピングし直して・・・という複雑な処理は必要ない

これをやるためには、ファイル自体の大きさ(バイト数)を予め知っておく必要があるが、その処理は前段で行なっている。ファイルの大きさ一つとってもこういう書き方をする。

なお、このコードの意味を理解したときはいたく感動した。

ポインタが使われている例としてよく知られたものだが、もう一つ例示しておくと main(argc, *argv[]) の意味もわかってみるとなんということはない。

こちらの記事あたり参照。

配列を舐めてはいけない

ところで、ポインタの話をすると、そこだけに注力してしまい、その他の知識が疎かになりやすい。

ポインタと配列の関連が深いのは上でも述べたが、実用的なプログラミングでは両方の知識を絡ませないとにっちもさっちもいかない。

C の配列で知っておかなければいけない知識は、

配列は初期化しないと使えない

という(ポインタに比べるとあまり注意を払われないんだが)超重要仕様。意外に理解してない人が多い。

初期化には、最初から全てを代入するとか初期化子を使う方法とかいくつかやり方があるんだが、最近のワイの好みは memset などを使って、根こそぎメモリ領域を確保しておく方法。

uint16_t testarray[100];
memset(testarray, 0, sizeof(uint16_t)*100);

あるいは(ややこしいが、こちらの方が実用的)、

uint16_t *testarray = (uint16_t *)calloc(100, sizeof(uint16_t));
// any code you want
free(testarray);

これで全ての要素に 0 が入るので、この初期化以降は testarray[50] だのに好きな値を代入できる。

もちろん、C の配列の使い方(可変長が許されない&初期化する必要がある)は、不便を感じることが多く、例えば MacOS/iOS のアプリを作成する際には、配列のみで攻めていく、ということはないと思う。

なお、最近、実感しているのは、NSData の優秀性。

長さが不定な重要データは NSData 型で管理し、具体的な処理は C ライブラリに任せるとかやると混乱は減る。

「ポインタ」は重要だが、ポインタだけじゃダメって話です。

参考:『配列を自由自在に作る』など。
C で配列が使いにくい→ malloc などで対応、という流れが簡潔に説明されている。
malloc + memset = calloc という感覚なのだが、具体例が出たときにでも追記したい。
こちらの記事もいい。

最初はピンとこないかもしれないが

配列[変数]

となっている配列を扱いたいときにここら辺の知識があやふやだと、うまく扱えない。

逆に「C 使えるようになってきたかな?」と実感できるのはここら辺の知識が身についてきたとき。

Xcode プロジェクトで C/C++ でファイル入出力を扱うとき

アップルの話が出てきたので、ついでに。。。

最近、プライベートでは Mac 使う場合が多いが、これ(↓)知ってないとハマるかも。

デフォでは、ファイルアクセスに制限がある

BAD_ACCESS 関係のエラーが出たら、設定を見直しておきましょう。

参考

C 言語における 16 進表記文字列⇄バイナリ列変換』この配列操作はしばしば話題になるが、これで決定かな。
magicarray を使ったロジックが美しい。