🐊

V4L2で画像を取得するサンプルプログラムをZigで書き直した

2023/02/12に公開

最近、こちらに丁寧なV4L2(Video for Linux 2)の解説記事が出ました。素晴らしい!
https://zenn.dev/turing_motors/articles/programming-v4l2

ここで紹介されているウェブカメラから画像を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を調べる方法はこちら。
https://qiita.com/tetsu_koba/items/01491f96daddf15677df

苦労したところ

最初は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)

このようにioctlVIDIOC_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は
うっかりコーディングミスを
絶対に許さない!」
https://logmi.jp/tech/articles/328160

関連

https://zenn.dev/tetsu_koba/articles/c039b865114b93
https://zenn.dev/tetsu_koba/articles/95ce0deb9a6704
https://zenn.dev/tetsu_koba/articles/421198dc669f19
https://zenn.dev/tetsu_koba/articles/214644ef10fc52
https://zenn.dev/tetsu_koba/articles/0e91b69ff72ae0
https://qiita.com/tetsu_koba/items/01491f96daddf15677df
https://zenn.dev/tetsu_koba/articles/29bc1c5192a7ab

Discussion