V4L2で画像を取得するサンプルプログラムをZigで書き直した
最近、こちらに丁寧なV4L2(Video for Linux 2)の解説記事が出ました。素晴らしい!
ここで紹介されているウェブカメラから画像を1枚取得するC言語で書かれたサンプルプログラムをZig言語で書き直してみました。
Zig版サンプルプログラム
Zigで書き直すついでに、コマンドライン引数からデバイス名、width, height, 出力するJPEGファイル名を指定するように変更しました。さらに後始末はdefer文を使って抜けが無いようにしました。
ビルド
zigはmasterのものを使いました。zig 0.10.1 ではビルドエラーになってしまったためです。
$ zig version
0.11.0-dev.1594+a5d25fabd
$ zig build-exe -lc v4l2sample.zig
実行
$ ./v4l2sample
Usage: ./v4l2sample /dev/videoX width height out.jpg
$ ./v4l2sample /dev/video2 1280 720 out1.jpg
$ file out1.jpg
out1.jpg: JPEG image data, baseline, precision 8, 1280x720, components 3
指定可能なwidth, heightを調べる方法はこちら。
苦労したところ
最初はzig translate-c を使って機械的にZigに変換してみました。変換とビルドはすんなりいったのですが、動かしてみるとエラーになってしまいました。
straceで調べてみると
407276 ioctl(3, VIDIOC_QUERYBUF, {type=V4L2_BUF_TYPE_VIDEO_CAPTURE, index=0, memory=V4L2_MEMORY_MMAP, m.offset=0, length=251733, bytesused=0, flags=V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC|V4L2_BUF_FLAG_TSTAMP_SRC_SOE, ...}) = 0
407276 mmap(NULL, 251733, PROT_READ|PROT_WRITE, MAP_SHARED, 3, 0) = 0x7f29429f2000
407276 ioctl(3, VIDIOC_QUERYBUF, {type=V4L2_BUF_TYPE_VIDEO_CAPTURE, index=1, memory=V4L2_MEMORY_MMAP, m.offset=0x3e000, length=251733, bytesused=0, flags=V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC|V4L2_BUF_FLAG_TSTAMP_SRC_SOE, ...}) = 0
407276 mmap(NULL, 251733, PROT_READ|PROT_WRITE, MAP_SHARED, 3, 0x3e000) = 0x7f29429b4000
407276 ioctl(3, VIDIOC_QUERYBUF, {type=V4L2_BUF_TYPE_VIDEO_CAPTURE, index=2, memory=V4L2_MEMORY_MMAP, m.offset=0x7c000, length=251733, bytesused=0, flags=V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC|V4L2_BUF_FLAG_TSTAMP_SRC_SOE, ...}) = 0
407276 mmap(NULL, 251733, PROT_READ|PROT_WRITE, MAP_SHARED, 3, 0x7c000) = 0x7f2942976000
407276 ioctl(3, VIDIOC_QBUF, {type=V4L2_BUF_TYPE_VIDEO_CAPTURE, index=0}) = -1 EBADR (Invalid request descriptor)
407276 dup(2) = 4
407276 fcntl(4, F_GETFL) = 0x2 (flags O_RDWR)
407276 newfstatat(4, "", {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0), ...}, AT_EMPTY_PATH) = 0
407276 write(4, "VIDIOC_QBUF: Invalid request des"..., 40) = 40
407276 close(4) = 0
407276 ioctl(3, VIDIOC_QBUF, {type=V4L2_BUF_TYPE_VIDEO_CAPTURE, index=1}) = -1 EBADR (Invalid request descriptor)
407276 dup(2) = 4
407276 fcntl(4, F_GETFL) = 0x2 (flags O_RDWR)
407276 newfstatat(4, "", {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0), ...}, AT_EMPTY_PATH) = 0
407276 write(4, "VIDIOC_QBUF: Invalid request des"..., 40) = 40
407276 close(4) = 0
407276 ioctl(3, VIDIOC_QBUF, {type=V4L2_BUF_TYPE_VIDEO_CAPTURE, index=2}) = -1 EBADR (Invalid request descriptor)
407276 dup(2) = 4
407276 fcntl(4, F_GETFL) = 0x2 (flags O_RDWR)
407276 newfstatat(4, "", {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0), ...}, AT_EMPTY_PATH) = 0
407276 write(4, "VIDIOC_QBUF: Invalid request des"..., 40) = 40
407276 close(4) = 0
407276 ioctl(3, VIDIOC_STREAMON, [V4L2_BUF_TYPE_VIDEO_CAPTURE]) = 0
407276 poll([{fd=3, events=POLLIN}], 1, 5000) = 1 ([{fd=3, revents=POLLERR}])
407276 ioctl(3, VIDIOC_DQBUF, {type=V4L2_BUF_TYPE_VIDEO_CAPTURE}) = ? ERESTARTSYS (To be restarted if SA_RESTART is set)
407276 --- SIGINT {si_signo=SIGINT, si_code=SI_KERNEL} ---
407276 +++ killed by SIGINT +++
ioctl(3, VIDIOC_QBUF, {type=V4L2_BUF_TYPE_VIDEO_CAPTURE, index=0}) = -1 EBADR (Invalid request descriptor)
このようにioctl
で VIDIOC_QBUF
しているところでエラーが返ってきています。これがうまくいかない直接的な原因のようです。
元のCのサンプルプログラムではこの部分は以下のようになっています。
407626 ioctl(3, VIDIOC_QUERYBUF, {type=V4L2_BUF_TYPE_VIDEO_CAPTURE, index=0, memory=V4L2_MEMORY_MMAP, m.offset=0, length=251733, bytesused=0, flags=V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC|V4L2_BUF_FLAG_TSTAMP_SRC_SOE, ...}) = 0
407626 mmap(NULL, 251733, PROT_READ|PROT_WRITE, MAP_SHARED, 3, 0) = 0x7fcb94c87000
407626 ioctl(3, VIDIOC_QUERYBUF, {type=V4L2_BUF_TYPE_VIDEO_CAPTURE, index=1, memory=V4L2_MEMORY_MMAP, m.offset=0x3e000, length=251733, bytesused=0, flags=V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC|V4L2_BUF_FLAG_TSTAMP_SRC_SOE, ...}) = 0
407626 mmap(NULL, 251733, PROT_READ|PROT_WRITE, MAP_SHARED, 3, 0x3e000) = 0x7fcb94c49000
407626 ioctl(3, VIDIOC_QUERYBUF, {type=V4L2_BUF_TYPE_VIDEO_CAPTURE, index=2, memory=V4L2_MEMORY_MMAP, m.offset=0x7c000, length=251733, bytesused=0, flags=V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC|V4L2_BUF_FLAG_TSTAMP_SRC_SOE, ...}) = 0
407626 mmap(NULL, 251733, PROT_READ|PROT_WRITE, MAP_SHARED, 3, 0x7c000) = 0x7fcb94c0b000
407626 ioctl(3, VIDIOC_QBUF, {type=V4L2_BUF_TYPE_VIDEO_CAPTURE, index=0, memory=V4L2_MEMORY_MMAP, m.offset=0, length=251733, bytesused=0, flags=V4L2_BUF_FLAG_MAPPED|V4L2_BUF_FLAG_QUEUED|V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC|V4L2_BUF_FLAG_TSTAMP_SRC_SOE, ...}) = 0
407626 ioctl(3, VIDIOC_QBUF, {type=V4L2_BUF_TYPE_VIDEO_CAPTURE, index=1, memory=V4L2_MEMORY_MMAP, m.offset=0x3e000, length=251733, bytesused=0, flags=V4L2_BUF_FLAG_MAPPED|V4L2_BUF_FLAG_QUEUED|V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC|V4L2_BUF_FLAG_TSTAMP_SRC_SOE, ...}) = 0
407626 ioctl(3, VIDIOC_QBUF, {type=V4L2_BUF_TYPE_VIDEO_CAPTURE, index=2, memory=V4L2_MEMORY_MMAP, m.offset=0x7c000, length=251733, bytesused=0, flags=V4L2_BUF_FLAG_MAPPED|V4L2_BUF_FLAG_QUEUED|V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC|V4L2_BUF_FLAG_TSTAMP_SRC_SOE, ...}) = 0
407626 ioctl(3, VIDIOC_STREAMON, [V4L2_BUF_TYPE_VIDEO_CAPTURE]) = 0
エラーにはなっていません。
安直に自動変換したものではダメなのかなあということで、手でちまちまとZigに書き直していきました。
すると、以下のようなコンパイルエラーが出ました。
$ /opt/zig-linux-x86_64-0.10.1/zig build-exe -lc v4l2sample.zig
/home/koba/.cache/zig/o/22e750b1e257ca59a3b27b21eddd171d/cimport.zig:2412:146: error: expected type 'c_uint', found 'c_int'
pub inline fn _IOC(dir: anytype, @"type": anytype, nr: anytype, size: anytype) @TypeOf((((dir << _IOC_DIRSHIFT) | (@"type" << _IOC_TYPESHIFT)) | (nr << _IOC_NRSHIFT)) | (size << _IOC_SIZESHIFT)) {
^~~~~~~~~~~~~~~~~~~~
/home/koba/.cache/zig/o/22e750b1e257ca59a3b27b21eddd171d/cimport.zig:2412:146: note: unsigned 32-bit int cannot represent all possible signed 32-bit values
referenced by:
_IOR: /home/koba/.cache/zig/o/22e750b1e257ca59a3b27b21eddd171d/cimport.zig:2422:74
VIDIOC_QUERYCAP: /home/koba/.cache/zig/o/22e750b1e257ca59a3b27b21eddd171d/cimport.zig:3715:29
remaining reference traces hidden; use '-freference-trace' to see all reference traces
try xioctl(fd, c.VIDIOC_QUERYCAP, @ptrToInt(&cap));
どうやらこの関数呼び出しの第二引数の定義がダメなようです。
あれこれ調べたのですが、最終的には0.10.1 でなくてmasterのzigを使ったら問題なくコンパイルできました。
(同じエラーで悩んでいる人が検索エンジン経由でこのページを見つけられるようにエラーメッセージを掲載しています。)
さて、コンパイルもできて全部書き直すことができたので動かしてみると、なんとzig translate-cで生成したものと全く同じエラーで動作しませんでした。
いろいろと試行錯誤した結果、
xioctl(fd, c.VIDIOC_QBUF, , @ptrToInt(&buf));
で渡しているbufのゼロクリアが足りていないことがわかりました。
そして、元のCのサンプルプログラムでも以下のような修正をすると、zig translate-cで自動変換したものもうまく動作するようになりました!
diff --git a/v4l2sample.c b/v4l2sample.c
index 1abac44..90d4563 100644
--- a/v4l2sample.c
+++ b/v4l2sample.c
@@ -115,7 +115,7 @@ void map_buffer(){
}
void enqueue_buffer(int index){
- struct v4l2_buffer buf;
+ struct v4l2_buffer buf = {0};
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = index;
ローカル変数の初期化漏れというバグですね。Cではたまたまうまく動いていたので気がつかなかったというもので、いつか(コンパイラのバージョンが変わったり、コンパイルオプション変えたときなどに)突然動かなくなるという地雷系バグでした。
参照元のブログの著者の方にも伝えようと思います。
宣伝
ここで宣伝です w
「Zigは
うっかりコーディングミスを
絶対に許さない!」
関連
Discussion