Pixel Pedals of Tomakomai

北海道苫小牧市出身の初老の日常

macOS Big Surでマウスポインタと違うところがクリックされる問題の解消

ゴミ箱macOS Big Sur にアップグレードをしたら、クリックした時にマウスカーソルが指している場所と全然違う部分がクリックされるようになって、詰みかけた。 guest アカウントでログインすると発生しないので、ハードウェア的な問題ではなさそう。

30 分以上格闘した後、マウスポインタをシェイクして見つける の機能で、カーソルが大きくなっている時は正しい場所を指し示していることがわかった。

その後さらに試行錯誤を続けて、ようやく関連している設定を見つけた。 Accessibility の Zoom の項目にある、 「Use scroll gesture with modifier keys to zoom」 を有効にしていると、この問題が発生するようだ。

f:id:hiratara:20210429140202p:plain

昔、プレゼンをする際に、 ライブコーディングなどでエディタや端末を拡大するために ズーム機能を使っていた人は多かったのではないかと思う。昔のマシンからアップデートをして壊れた人は、この項目をチェックしてみるといいだろう。

ROG Strix SCAR 15 G533 を買った

rog.asus.com

見ての通り、完全にゲーミングノートである。去年 Surface Book 3 を買ったばかり なので買う必要はまったくなかったのだが、コロナ禍のお陰で PC を触る時間が極端に増えたのと、 PS5 も switch のマリオレッド×ブルー セットも買い逃してついカッとなったと言うのもある。

ASUS ストアで買ったので、実は 30日返品保証 が使えるのだが、今の所返すつもりはない。最近の人にとっては違うのだろうが、自分にとってあくまでも PC は道具ではなく娯楽そのものであり、ワクワクするものである。そういう理由でつまらなくなった AppleMac を買うのを辞めてしばらく Surface シリーズを買い続けていたのだが、 Surface シリーズはいい意味で変態的ではあるが、あくまでも作業用の道具という枠を超えてない。いっそのこと娯楽に振り切ってしまってもいいのではないか、と思った次第である。

キーボードが US 配列なのも決め手となった。スペックは申し分なく(Ryzen 9 5900HX)、もちろんゲームができるが、開発環境としても快適に使える。 Surface Book 3 と比べると解像度や画面の縦幅が狭かったりするが、 WSL2 と Docker 、 VS Code さえ入れてしまえば立派な開発機となる。 2 Kg を超えバッテリーも重いので持ち運びに優しいとは言えないが、ギリギリ不可能ではない範囲に入るだろう。カメラがないことは注意が必要かもしれない。とは言え、プログラミングをやるようなコミュ障な人間が、何を好んで自分を撮る必要があるのか。不要だろう 1

そして、光る。誰がどういう理由でコンピューターを光らせ始めたのかとんと検討もつかないが、カラフルな光を見つめるのは楽しい。白状すると、加湿器も LED で 7 色に光るものを使っている。センスがない、もしくは中2なのかもしれない。いずれにしても、本人が満足していれば、それでいいのだろう。

ROG Strix SCAR 15 G533


  1. 冗談はさておき、コロナ禍で在宅勤務をすることを考えるとカメラは重要で、これ一台でコロナ禍を乗り切るのは厳しいだろう。

CellとRefCell

CellRefCell はどちらも「内側のミュータビリティ」を実現するものだが、実現方法が違う。

RefCell については、ランタイムでボローチェックしていることはよく知られている(と思う)。

    let ref_cell = RefCell::new(20i32);
    println!("ref_cell (1): {}", ref_cell.borrow());

    println!("ref_cell (2): {}", ref_cell.borrow());

    *ref_cell.borrow_mut() = 200;
    println!("ref_cell (3): {}", ref_cell.borrow());
ref_cell (1): 20
ref_cell (2): 20
ref_cell (3): 200

ランタイムでとは、文字通りコンパイラではなくランタイムで、つまりプログラムで手動で所有権を管理しているということである。

rust/cell.rs at dd4851d503f3fae0c0c742a19e0d8e6e2140bd2a · rust-lang/rust · GitHub

UnsafeCell というのは、この値は「内側のミュータビリティ」として使うよという意思表示であり、要は生ポインタで処理しますっていうこと。 borrow というのが借用の数で、正の数の場合は immutable な参照の個数で、負の数の場合は mutable な参照の個数(通常は借用ルールから 1 個まで)と管理されている。

rust/cell.rs at dd4851d503f3fae0c0c742a19e0d8e6e2140bd2a · rust-lang/rust · GitHub

じゃあ、 Cell はどうかというと、こちらは copy または move によって「内側のミュータビリティ」を実現する。 immutable で定義した値について、 copy または move することによって保持する値を無理矢理変更することができる。

    let cell = Cell::new(10i32);
    println!("cell (1): {}", cell.take());

    println!("cell (2): {}", cell.take());

    cell.set(100);
    println!("cell (3): {}", cell.take());
cell (1): 10
cell (2): 0
cell (3): 100

cell (2) の出力が 0 になっていることに着目したい。これは take() されることにより、 Cell の保持する値がデフォルト値に戻ったことを意味する。変数 cell はイミュータブルで定義されているが、 println! する度に値が変わっており、きちんと「内側のミュータビリティ」を実現できていることがわかる。

実装は RefCell より遥かにシンプルなので、興味がある人は読んでみよう。保持している値が Copy なのか Default なのかで、2つの異なる API get()take() が実装されているのが面白い。

参考: Rust Cell and RefCell

Data.Vaultの非GHC実装

ふと、 Data.Vault ってどうやって実装してるんだろと中身を覗いていたら、なんかすごいものを見つけた。

vault/IORef.hs at 39cf64b47c24b83c24924d47d2385f8213a3f322 · HeinrichApfelmus/vault · GitHub

今はほぼ GHC 一択なのでこの実装を使っている人はほとんどいないんだろうけど、 issue を読む限り UHC 向けに実装されたもののようだ。

Vault の定義を見ると、一瞬ぎょっとなる。

data Locker s = Locker !Unique (IO ())
newtype Vault s = Vault (Map Unique (Locker s))

まさかの IO () なんだが。どうしてこれで値を保存できるかというと、 Key の方に IORef があるからである。

data Key s a  = Key    !Unique (IORef (Maybe a))
unlock (Key k ref) (Locker k' m)
    | k == k' = unsafePerformIO $ do
        m
        readIORef ref     -- FIXME: race condition!
    | otherwise = Nothing
lookup key@(Key k _)   (Vault m) = unlock key =<< Map.lookup k m

lookup すると、 Vault に保存されている IO ()unsafePerformIO で実行した上で IORef から値を読むことになる。ここまで来るともう察しが付くように、 insert はこのように IORef に値を書き込む IO アクションを保存している。

lock (Key u ref) x = Locker u $ writeIORef ref $ Just x
insert key@(Key k _) x (Vault m) = Vault $ Map.insert k (lock key x) m

こうすることで、 Vault の値の型を IO () 単一に固定しつつ、 Key a 側の型 a の値を読めるようになっている。この発想はなかった。ただ、だいぶ雑に IORef を読み書きしているので、まあ、レースコンディションになるわなって感じではある。

ちなみに、 GHC 向けの実装は GHC.Exts.Any を使った平和な実装になっている。知りたかったのはこっちだ。

vault/GHC.h at 39cf64b47c24b83c24924d47d2385f8213a3f322 · HeinrichApfelmus/vault · GitHub

謹賀新年

明けましておめでとうございます。本年もよろしくお願い致します。

新型コロナで在宅勤務が始まってからは、自然と育児にコミットする時間が激増していて、まあ、間近で子供の成長を見られるのは他では体験できないとても幸せなことではあるのですが、一方で、本当にこんな人生を歩んでいいんだっけと日々自問自答しております。

一言で言えば、数学やHaskellやRustなどきちんと興味を持てるものにどっぷりと浸かりたいので、新年を機に時間を作っていこうと思います。

cargo test --jobs N -- --test-threads=M

redis-rs の cluster のテストが何度やっても通らなくてハマった。ただし、テスト名を指定して一個ずつであれば成功する。該当するテストは以下。

redis-rs/test_cluster.rs at bd8dc731ded564329d74717dfacc5cce748d891f · mitsuhiko/redis-rs · GitHub

このテストは redis server を立ち上げてクラスタを組んでテストを実行している。このとき、各サーバには 7000 番ポートから順番に割り当てている。

redis-rs/cluster.rs at bd8dc731ded564329d74717dfacc5cce748d891f · mitsuhiko/redis-rs · GitHub

そして、デフォルトでは cargo test 1 は複数のテストを並列に実行する。直列な世界に生きているので、ここに気がつくまでに時間がかかってしまった。

テストの実行ファイルでヘルプを表示させると、デフォルトでは並列実装すると書いてある。

$ target/debug/deps/test_cluster-cdbee22b93a5c3e7 --help
Usage: --help [OPTIONS] [FILTER]                                                                                                                                                                                                                        
Options:
..snip..
        --test-threads n_threads
                        Number of threads used for running tests in parallel
..snip..
By default, all tests are run in parallel. This can be altered with the
--test-threads flag or the RUST_TEST_THREADS environment variable when running
tests (set it to 1).
..snip..

実際、 rustc 側で実装を見ると、デフォルトは CPU コア数であることがわかる。

rust/concurrency.rs at 4ae328bef47dffcbf363e5ae873f419c06a5511d · rust-lang/rust · GitHub

並列で実行すると test_cluster.rs が落ちるのは明確だ。 7000 番 port が競合しているので、それぞれのテストで使う redis のサーバが建てられなくなってしまう。直列で実行するしかない。

redis-rs のリポジトリには、 Makefile があり、そこで --test-threads が指定されている。これはテストの実行ファイルが持つオプションなので cargo に直接渡すことはできず、 -- --test-threads=1 というように -- の後ろに指定しなければならない。

redis-rs/Makefile at bd8dc731ded564329d74717dfacc5cce748d891f · mitsuhiko/redis-rs · GitHub

ところで、 --test-threads=1 とは別に、 cargo には --jobs というオプションがある。

$ cargo test --help
..snip..
    -j, --jobs <N>                   Number of parallel jobs, defaults to # of CPUs
..snip..

ドキュメントは以下にある。紛らわしいが、実行ファイルのビルドに使う CPU 数を指定するオプションらしい。

doc.rust-lang.org

The --jobs argument affects the building of the test executable but does not affect how many threads are used when running the tests.

今回のように「単体テストが逐次実行しかできない」という場合には --jobs ではなく --test-threads を使わねばならない。指定先も cargo に指定するのか単体テストの実行ファイル側に指定するのかも別れていてわかりにくい。

そもそも、「単体テストが逐次実行しかできない」という状況がイケてないなあとは思う。


  1. 正確には、テストの実行ファイルである。

Haskellの古いmkSocketの引数のfamily

Linuxネットワークプログラミングの勉強をきちんとしましょうということなんだけど、とりあえず調べたことをメモっておく。

HaskellmkSocket という関数がある。

https://hackage.haskell.org/package/network-3.1.2.0/docs/Network-Socket.html#v:mkSocket

今は引数がファイルディスクリプタ CInt だけとなっているが、 2.x 系の頃の引数はもっと複雑だった。

https://hackage.haskell.org/package/network-2.2.3/docs/Network-Socket.html#v:mkSocket

この中に Family という引数があって、これは AF_INET とか AF_INET6AF_UNIX と言ったようなソケットの種類を表す値なんだけど、 Server::Starter のようにファイルディスクリプタが渡されている場合にどうやってこれらの値を決めるかが今回の疑問点。

実際、拙作の hs-server-starter では、 Server::Starter から渡された環境変数の値 0.0.0.0:80=3 というような値を雑にパースしてこれらの値を推測していたのだけど、 IPv6 アドレスの判定が雑過ぎて完全にバグっていた。

https://github.com/hiratara/hs-server-starter/blob/f47d198aab92135e6546191f82f44bec60ccaf5f/src/Network/ServerStarter/Socket.hs#L83-L101

じゃあ、どうやって直せばいいかってところなんだけど、 Starlet のコードを見ると、渡ってきたファイルディスクリプタはすべて IPv4 であると見なしてコードが組まれているように見えた。これに従えば、 AF_INET 決め打ちにしても差し支えないように思える。

https://metacpan.org/source/KAZUHO/Starlet-0.31/lib/Plack/Handler/Starlet.pm#L26-30

現に、以下のように改修をしても、テストやサンプルアプリは問題なく動いた(もちろん、 network-2.x 系を使って試している)。

https://github.com/hiratara/hs-server-starter/pull/10/files

じゃあ、この family は何に使っているのかということで、例えば以下にある getSocketName を見てみる。

https://hackage.haskell.org/package/network-2.2.3/docs/src/Network-Socket.html#getSocketName

withNewSockAddr を見ると、 sizeOfSockAddrByFamily でサイズを求めるのに使っている。直前の定義を見ると AF_INET = 16 AF_INET6 = 28 AF_UNIX = 110 などと family ごとにサイズが違っている。

https://hackage.haskell.org/package/network-2.2.3/docs/src/Network-Socket-Internal.html#withNewSockAddr

そして、このサイズを元に struct sockaddr を確保して c_getsockname を呼ぶことになる。この getsockname を調べると、

https://man7.org/linux/man-pages//man2/getsockname.2.html

The returned address is truncated if the buffer provided is too small;

つまり、渡した struct sockaddr のサイズが足りなくても、渡した文のサイズで切られて返されるだけということである。そういう意味では AF_INET は一番サイズが短いので、 AF_UNIX などを採用しておいたほうが害が少ないのかもしれない。

family はこのようにシステムコールを呼ぶのに必要な struct sockaddr のサイズを決めるためにしか使われておらず、実際はシステムコールによって返された struct sockaddr を以下の関数 peekSockAddr で解釈して使うことになる。 struct sockaddrfamily によって形式が違うが、先頭は sa_family_t となっているので、これを見ることで family を判別できる。

https://hackage.haskell.org/package/network-2.2.3/docs/src/Network-Socket-Internal.html#peekSockAddr

ちなみに、 network-3.x 系では family などを渡さなくて良くなっているが、それは十分に大きい struct sockaddr を確保しているからである。

https://hackage.haskell.org/package/network-3.1.2.0/docs/src/Network.Socket.Types.html

-- sizeof(struct sockaddr_storage) which has enough space to contain
-- sockaddr_in, sockaddr_in6 and sockaddr_un.
sockaddrStorageLen :: Int
sockaddrStorageLen = 128