Rustでコマンドラインアプリでキーが押されたかの判定しつつ、別の処理も進めるには
おはようございます。先日長男は、保育園のキャンプに参加して、初めての保育園外での泊りをしてきました。ここ数年、天候が微妙だった園のキャンプでしたが、今年は天気に恵まれ、楽しんで帰ってきました。@kjunichiです。
背景
RustでWebカメラの画像をコマンドラインに表示するコマンドを作っている。 このコマンドをESCキーが押されたら、終了するようにしたい。
アプリ側で、映像を表示させつつ、キー入力を監視して、 特定のキーが押されたら、終了したい。
(シグナルを使って、SIGINT時の処理にこの終了処理を加えても対応できるが、Windowsの 事を考えると、キー操作の統一感が無い気がして、今回は見送った。)
Windowsのcmd.exe上で動かす場合、Win32APIのキー入力API(GetAsyncKeyState)でキーが押されたかの判定を ブロックせずにできた。
pub fn term_get_esc_key() -> bool { let r: i16 = unsafe { ffi::GetAsyncKeyState(ffi::VK_ESCAPE) } as i16; if r != 0 { return true; } return false; }
同じようなことを、macOSやLinux等のUNIX系の環境で行いたい。
これらの環境では、標準入力に文字が入力されることで判定する方法がある。 しかし、何も考えず標準入力からの入力を受け取ろうとするとブロックされてしまう。 それなら、スレッドでとやったら、
といったツイートの様に単にスレッドで入力待ちを行っても多くの環境でプロセス単位にブロックされ、 入力待ち以外の処理が行えないことを経験した。
分かった事
readシステムコールでのブロックを防ぐ
標準入力はプロセス実行時に既にオープンされており、このようなファイルデスクリプタはfcntlで以下の様に ノンブロックの指定をすることで、以降のreadでブロックされなくなる。
fcntl(0, F_SETFL, O_NONBLOCK);
エンターキーが押されるのを待たない
readをノンブロックにしても、まだ駄目で、エンターキーが入力されるまで、入力を検知できない。これは 通常使用している端末がそういった動きになるように設定しているからで、端末の属性をプログラム実行時に 変更する必要があった。
幸い以下のページを見つけて、端末の属性を変更する方法が分かった。
プログラム開始後に端末の設定を変えており、プログラム終了後にこの状態を元に戻す必要がある。 戻さないと、コマンド入力がエンターまで待ってくれず、まともに出来なくなることがあるらしい。
macOSの場合、そういった状況に陥ったらreset+エンターで割と復活することが多い。
現在の端末属性を保持する
tcgetattr(0, &saved_term);
エンターまで入力を待たずに受け取る
term.c_lflag &= ~(ICANON | ECHO);
tcsetattr(0, TCSANOW, &term);
プログラム終了時に端末属性を元に戻す
tcsetattr(0, TCSANOW, &saved_term);
以上のことをRustでやった
libcクレートにこれらのAPIが定義されていた。 定数も皆今回必要なモノは定義済みで助かった。
extern crate libc; use std::{thread, time}; use std::io::Write; use std::time::SystemTime; fn main() { println!("Hello, world!"); let mut saved_termattr = libc::termios { c_iflag: 0, c_oflag: 0, c_cflag: 0, c_lflag: 0, c_cc: [0u8; 20], c_ispeed: 0, c_ospeed: 0, }; unsafe { let mut ptr = &mut saved_termattr; libc::tcgetattr(0, ptr); } let mut termattr = saved_termattr; termattr.c_lflag = termattr.c_lflag & !(libc::ICANON | libc::ECHO); termattr.c_cc[libc::VMIN] = 1; termattr.c_cc[libc::VTIME] = 0; unsafe { libc::tcsetattr(0, libc::TCSANOW, &termattr); } unsafe { libc::fcntl(0, libc::F_SETFL, libc::O_NONBLOCK); } let mut buf: [libc::c_char; 1] = [0; 1]; let ptr = &mut buf; loop { let r = unsafe { libc::read(0, ptr.as_ptr() as *mut libc::c_void, 1) }; if r > 0 { println!("input !{:?}", *ptr); break; } thread::sleep(time::Duration::from_millis(300)); let code: [char; 3] = [0x1b as char, '[', 'K']; print!("\r"); for i in 0..3 { print!("{}", code[i]); } print!("{:?}", SystemTime::now()); std::io::stdout().flush().unwrap(); } unsafe { libc::tcsetattr(0, libc::TCSANOW, &saved_termattr); } }
出来た!
まとめ
cursesやncurses使うと、対応できることも薄々分かっていたが、今回termbox-goを使う前提だったので、 これらのライブラリを混ぜるな危険との認識で避けていた。
その、結果、上記のページに出会って、Cでの素朴なやり方を知ることが出来、Rustでも実装出来た。
実は、今回のコマンドプロンプトや、コンソールに画像を表示する為に内部的に使用しているtermbox-goという ライブラリでは、端末の属性変更しており、fcntlシステムコールを発行して標準入力をノンブロックにするだけで良かった。
関連記事
- Go言語でコマンドプロンプトに画像を表示するコマンドを作って、Vimの:terminalで表示させた
- WindowsでmrubyからGoで作ったライブラリを呼び出して、Webカメラの画像をコマンドプロンプトに表示した
- 2018年版、Rustで多次元配列を使うには
- Surface Goのカメラ映像をRustでコマンドプロンプトに表示した
10年前の記事
9年前の記事
7年前の記事
5年前の記事
mruby-webcamにWebカメラで取得する画像サイズを指定できるメソッドを追加した
おはようございます。このところ、すっかり朝晩が涼しく、リビングから寝室に戻り、長男用の簡易ベッドをベッドわきに作って家族で寝室で寝るようになりました。このおかげで、ベッドの自分のスペースがだいぶ広くなりました@kjunichiです。
背景
前回のコマンドプロンプトにWebカメラを表示させた際の表示速度と比べてsixelでの表示が遅いので、 Webカメラから取得する画像サイズを減らして、表示速度の改善を図った。
対応
- Webcamクラスに@width,@heightを追加
attr_accessor :width, :height
- Videocaptureインスタンスにこの、@widht,@heightの設定値を設定
mrb_value v = mrb_iv_get(mrb, self, mrb_intern_lit(mrb, "@width")); if (mrb_fixnum_p(v)) { width = mrb_fixnum(v); } v = mrb_iv_get(mrb, self, mrb_intern_lit(mrb, "@height")); if (mrb_fixnum_p(v)) { height = mrb_fixnum(v); } if (width > 0) { cap.set(CV_CAP_PROP_FRAME_WIDTH, width); } if (height > 0) { cap.set(CV_CAP_PROP_FRAME_HEIGHT, height); }
結果
まとめ
当初、@width,@heightの値の取得を誤って、fixnum型の判定の関数を読んでいて、いくらサイズを変えても、 ターミナルの端っこに小さな画像しか表示されず、半日以上解決にかかってしまった。
関連記事
WindowsでmrubyからGoで作ったライブラリを呼び出して、Webカメラの画像をコマンドプロンプトに表示した
おはようございます。相変わらずリビングでの雑魚寝なのですが、乳幼児用の敷布団と大人の布団1枚に子供2人と寝ていると、狭くなってきたので、もう一枚大人用の布団を購入して、大人用の布団2枚に子供たちと3人で寝ています。@kjunichiです。
背景
Windowsのコマンドプロンプトにファイルや、URLを指定して、画像をコマンドプロンプトに表示する以下の記事を書いた。
これを、mrubyから利用できれば、、作成済みのWebカメラの画像を取り込めるmrbgemのmrbgemmruby-webcamで取り込んだWebカメラの画像をLinuxやmacOSのようにコマンドプロンプトに表示できるのでは
準備
Go側は、jpegやpngをファイルを入力元としている、このままでは、mrubyからもファイルを経由して呼び出すことになり、 SSDへの負担が懸念されるw。もちろんスピードも。
Go側のAPIをなんとかする
前述のようにさすがにmruby側からファイルのパスを貰う仕掛けでは今回の様にWebカメラの画像を表示したいという用途では、 ファイルを介してのやり取りは避けたい。
mruby側で、unsigned charな配列と、画像サイズを渡して、Go側でImage型を作って、それを既存のGoの処理に流すという方法で 対応することにした。
Cから渡された先頭アドレスをGoのスライスとして扱うには
などを眺めて、どうやら、
の書き方で良さそう(容量が2Gバイトまでの制限がありそうだが。。)
func termPutImage(imageData *C.uchar, width, height C.int) { len := 3 * width * height slice := (*[1 << 30]C.uchar)(unsafe.Pointer(imageData))[:len:len]
Go言語のImage型に変換するには
以下の様に、C側からもらった配列(PPMのP6形式)をImage型に変換出来た。
img := image.NewRGBA(image.Rect(0, 0, int(width), int(height))) for h := 0; h < int(height); h++ { for w := 0; w < int(width); w++ { r := uint8(slice[3*(h*int(width)+w)]) g := uint8(slice[3*(h*int(width)+w)+1]) b := uint8(slice[3*(h*int(width)+w)+2]) c := color.RGBA{r, g, b, 0} img.SetRGBA(w, h, c) } }
セットする際は0-255なのかぁ。取得時は0-65535なのに
libhogehoge.a形式をGoでつくるには
Windowsでは共有ライブラリこそ作成できないが、アーカイブ形式のライブラリの出力はサポートされている。
go build -buildmode=c-archive -o libimgtype.a main.go
これで、.a形式でライブラリが作成される。MinGWのgcc環境なら、これを即利用できる。
この辺りは
の記事が大変参考になりました。ありがとうございます。
MSVCなmrubyなので、.libファイルを作ってリンクさせる
今回使おうとしているmruby-webcamというmrbgemはMSVC版のOpenCVを使っており、それに合わせて、MSVCを使うことに なる。
その為、defファイルを用意してこれと、先ほどの.a形式のライブラリから.dll形式を生成。また、MSVCの処理系でのリンク時に必要な.libファイルを.defから生成しておく。 といった具合になかなか面倒な作業が必要となる。
dllを作る
DLLはMinGW版のgcc(cgoで指定しているgcc)で行った。
gcc -m64 -shared -o imgtype.dll imgtype.def libimgtype.a -Wl,--allow-multiple-definition -static -lwinmm -lntdll -lWs2_32
libファイルを作る
.libファイルはMSVC付属のlibコマンドで行う。
lib /machine:x64 /def:imgtype.def
これで、MSVCなmrubyに組み込む準備が出来た。
mrubyからGoの呼び出し
ここまでくれば、あとは、mrbgemを書くだけ。 作成した.libファイルをmrbgem.rakeに以下の様に記述して
spec.linker.flags_before_libraries << "imgtype.lib"
環境変数LIBにimgtype.libファイルへのパスを通しておき、rakeでGoのライブラリを組み込んだmruby.exe,mirb.exeが出来る。
動かす
今回必要なmrbgemは以下
conf.gembox 'default' conf.gem '../mruby-webcam' conf.gem '../mruby-imgtype' conf.gem :github => 'matsumoto-r/mruby-sleep'
Webカメラの映像をコマンドプロンプトやVimの:terminalに表示するmrubyのスクリプトは以下。
Imgtype.init cam = Webcam.new cam.setFmt "ppm" cam.capture {|img| Imgtype.imgtype img } while true cam.snap Sleep::usleep(1000) if Imgtype.get_key == "ESC" then break end end Imgtype.close
実行結果
Imgtype.get_keyがWindowsのAPIを直接読んで対応している。キー入力をポーリングせずに取得するのに意外とハマった。
Windowsの:terminalに対応したVimにWebカメラの映像を出してみた https://t.co/gtYXg0tOA3 @YouTubeさんから
— kjunichi (@kjunichi) 2017年8月31日
まとめ
今回はGo言語でライブラリを作成し、それをmrubyから呼び出し、 そこそこ実用的な処理を動かすという実績が作れた。
今回の作業を通して、Go言語でのスライスの扱いも少しわかってきました。
参考資料
関連記事
- mrubyでWebカメラを黒い画面にそのまま表示できるようにした
- RustでWebカメラの映像をコマンドプロンプトに出すコマンドを作った
- goroutineを使って高速にLAN内の*.localなホスト名の一覧を取得するツールを作った