Go のコードを Rust から呼び出す

近年のツールは Go で実装されていることが多く [要出典]、CLI だけでなくライブラリとして API も公開されていることも多くてそのツールを自作のツールに組み込むことも容易になっている。 しかしそれはあくまで自作のツールも Go で書いている場合で、言語の壁を超えることは難しい。

と思ってたんだけど、cgo を使えば Go でわりと手軽に C ライブラリを作ることができて、C ライブラリの呼び出しは色々な言語でサポートされているので、C のインターフェイスを経由すれば思ってたよりは手軽に他言語から Go のコードを呼び出せることに気付いた。

github.com

これは https://pkg.go.dev/cuelang.org/go/cue を使って cue export 相当を Rust からできるようにした例。 C の貧弱な表現に合わせたりメモリ管理の手間があったり Rust の文字列へのコピーのオーバーヘッドがあったりするけど、bindgen を使えば FFI の宣言を自動生成できるので、これくらいのコード量で Rust から Go のコードを呼び出せるようにできた。

やってることとしては、build.rs の中で go build -buildmode=c-archive を実行して .a と .h を生成し、.a を静的リンク対象に追加しつつ .h から bindgen で FFI の宣言を生成しているというかんじ。 Go の世界には C における const の概念が無いので、const char * を受け取るような関数を定義するときは typedef const char *const_string_t みたいな定義を preamble に書いておいて C.const_string_t を受け取る関数として定義して C の世界に export すればいい。 preamble とか export とかを特殊なコメントとしてやる設計もやはりあまり好きになれない……

ここまでで crate として動くものはできるんだけど、この crate を publish しようとすると docs.rs の環境でビルドできないという問題がある。docs.rs のビルド環境ではネットワークアクセスが禁止されていて、build.rs の中から Go module のダウンロードができないからだ。 これを回避するには go mod vendor で依存モジュールのコードも含めてパッケージングして publish するしかない、と思う。 あと地味な問題として go build がデフォルトでビルドに使うディレクトリが docs.rs の環境では書き込みを禁じられているので、if std::env::var("DOCS_RS").is_ok() { cmd.env("XDG_CACHE_HOME", "/tmp/.cache"); } みたいに XDG_CACHE_HOME を変えておく必要がある。

switch_point では ActiveRecord v6.1 以降をサポートしないことにした

switch_point を4年ぶりにリリースした。このリリースは主に ActiveRecord v6.1 以降をサポートしない意志を表明するためのものである。

github.com

switch_point は6年前に仕事で困ったことを解決するために書いた gem である。経緯は https://eagletmt.hateblo.jp/entry/2014/09/22/203819 を参照。この記事にある「Rails の激しい変更についていきやすい設計・実装」は成功したと思っていて、バージョンや respond_to? による分岐を一切せずに ActiveRecord v3.2 から v6.0 までサポートすることに成功している。

しかしまもなくリリースされるであろう ActiveRecord v6.1 では ActiveRecord::ConnectionAdapters::ConnectionPool#spec が消えて、ついに switch_point が壊れることが分かった。バージョン分岐を許容すれば直せそうな気がするが、しかし ActiveRecord v6.0 で本家に R/W Splitting 用の機能が入った https://guides.rubyonrails.org/active_record_multiple_databases.html 以上、switch_point を使えるようメンテする意味も無いと思い、ここで switch_point は終了させようと思った。

正直 v6.0 でこの機能が入った時点で switch_point は使えなくなるかと思っていたけど、運良く壊れずに済んでいた。よって ActiveRecord v6.0 では本家の機能と switch_point の両方を使えるので、switch_point からの移行パスとしてはまず ActiveRecord を v6.0 にし、この状態で switch_point から本家の機能へと移行することを想定している。

音楽サービス2020

自分は音楽の購入・管理サービスとして Google Play Music を使っていた。楽曲だけでなく、ドラマ CD だったり音声作品だったりにも Google Play Music を使っていた。しかし Google Play Music がサービスクローズになり、YouTube Music へと移行せざるをえなくなった。ストア機能が失われることに不満を感じつつも他に選択肢も無いので YouTube Music に移行したところ、音楽の再生に関する機能も若干使いづらい上に (Google ではなく) YouTube のアカウントと紐付いてしまって破滅したりアップロード機能が致命的に機能不足だったりと最悪の体験だった。あまりに最悪すぎて Apple Music へと徐々に移行していっているところなんだけど、そのへんの話を書いてみる。

音楽の購入・管理サービスの種別

自分にとって大きく分けて3つのコア機能があると思っている。

ストリーミングサービス

おそらく近年一番人気で需要が高いサービス。サブスクリプションサービスとも。定額の月額料金で対象の音楽をいくらでも聞けるというもの。この機能は Google Play Music にもあったし YouTube Music にもある。有名なサービスとしては SpotifyApple Music、Prime Music や Amazon Music Unlimited あたりだろうか。ANiUTa のようにジャンルを絞って独自のラインナップを提供しているサービスもある。

ストアサービス

サブスクリプションサービスが一般的になるまで一般的だったもの。CD ではなく mp3 等のデータの状態で音楽を購入できるサービス。購入した音楽は専用のアプリでどこでも再生できるようになることが多い。 Google Play Music の頃は Play Store で音楽を買ってそれを Google Play Music 内でそのまま再生できた。Play Store で音楽を扱わなくなったので、YouTube Music ではこの機能が失われた。有名なサービスとしては iTunes StoreAmazon Music、mora あたりだろうか。DLsite や Bandcamp のようなものもここに含まれるかも。

ロッカーサービス

手元にある音楽データを預けて色々なデバイスで再生できるようにしてくれるもの。ストアサービスで購入したデータをアップロードしたり、CD から取り込んだデータをアップロードしたりして、主にスマートフォンから簡単に再生できるようにするような使い方が一般的だと思う。 Google Play Music にはこの機能があったし YouTube Music にも一応あるが、YouTube Music 側はアップロード後のメタデータ編集に難があったりと機能不足感が強い。Amazon には Amazon Music Storage という名前でこの機能があったが、既にサービスクローズしており現在は使えない (はず)。Apple Music には iCloud ミュージックライブラリという名前で今もロッカーサービスがある。YouTube Music と Apple Music 以外でロッカーサービスを提供している有名なサービスを自分は知らない。

そして Apple Music へ

自分にとってはこの3つの機能をすべて持ったサービスが必要だった。ストアサービスについてはロッカーサービスがあれば他のストアサービスで購入した音楽を預ければいいので最悪無くてもいいが、手間を考えるとあったほうがいい。これらを満たしているものが、自分の知る限り Google Play MusicAmazon MusicApple Music の3つだけであり、現在もサービスを継続しているのが Apple Music しかない。iTunes Store 等で音楽を買って YouTube Music にアップロードするという手もあるけど、前述のように YouTube Music のロッカーサービスが微妙なので、YouTube Music を見限って Apple Music に徐々に移行しているのが今である。PC は WindowsLinuxスマートフォンAndroidスマートスピーカーGoogle Nest Hub と Apple 製品とは無縁の生活をしているけど、Android 用の Apple Music アプリがまぁまぁよくできているので決断できた。Chromecast デバイスへのキャストも実装していてえらい。

Apple Music で必要な機能はほぼ満たせているものの、Google Play Music と比較しての不満点もある。一番の不満は PC の iTunes から iCloud ミュージックライブラリへのアップロードが異常に遅いこと。https://eagletmt.hateblo.jp/entry/2020/07/06/025926 のスペックを持ち 400 Mbps くらいの上り速度が出る回線があっても本当にアップロードが遅いのでサーバ側の処理が遅いんだろうけど、わけわからんくらい遅い上によく失敗してガチャガチャ操作しないと進行しなくなる。Google Play MusicChrome 拡張必須ではあるけど Chrome からさくさくアップロードできていたのに *1。他にも iTunes のあらゆる動作が重かったり Apple Music の Web サイトが重かったりとパフォーマンスに関する細かい不満は色々あるが iCloud ミュージックライブラリへのアップロードの遅さと比べれば些細だし、アップロードの遅さがあっても他に必要な機能を提供しているサービスがないので Apple Music を選択している…… YouTube Music で唯一助かっているのは料金が YouTube Premium と抱き合わせになっていること。YouTube Premium を解約する気は今のところ無いので、YouTube Music に預けた音楽をキープしつついつか YouTube Music の機能が改善されることを祈り続けることができる。

*1:本当は Firefox からアップロードできてほしかったけど…… YouTube Music では Firefox からもアップロードできるようになっていた

身分証をすべて紛失したときの思い出

皆さんは財布を紛失したことはありますか? 私はあります。 紛失してからいつのまにか2年以上経過していたので、当時の思い出を書いてみる。

紛失したとき

自分の場合は電車で出掛けた先の某ゲームセンターで財布がなくなっていることに気付いた。自宅から出たときにはたしかに持っていたはずだけど、Suica やゲーセン用の小銭は別に持っていたので、自宅から最寄り駅までの間で紛失したのか、電車内で紛失したのか、降りた駅からゲーセンまでの間で紛失したのか、ゲーセン内で紛失したのか分からなかった。ゲーセン内の心当たりある場所を探しても見つからず、店員に落とし物のことを聞いても見つからず、この時点で紛失したと判断した。 紛失した財布の中にはクレジットカードが入っていたため、自分の場合はまずクレカの無効化と再発行を電話で依頼した。そして近くの交番へ行って遺失届を出した。たぶん先に遺失届を出すほうが正解だった。その場で何を紛失したか具体的に書くわけだけど、自分の場合は

  • 現金
  • クレジットカード
  • キャッシュカード
  • 運転免許証
  • 保険証

を同時に紛失することになった。Suica は別に持っていたので自宅まで帰るには困らなかったし、自宅に別のクレジットカードを置いてあったので金銭面ではそこまで困難な状況にはならなかったのだが、一時的にキャッシュカードと身分証が何も無い状態になった。

再発行の手続き

現金はもう諦めるとして、紛失した各種カードを止めて再発行していく作業が必要になる。クレジットカードやキャッシュカードは各カード会社や銀行によって異なるだろうけど、自分の場合は再発行に必要なものは以下のようだった。

クレジットカード

現住所の情報をちゃんと正しい状態にしてあれば、電話一本で再発行できて1、2週間で手元に届いた。今回紛失した中で一番手軽だった。

キャッシュカード

止めるまでは電話でなんとかなったが、再発行の手続きの際に身分証が必要なパターンと、再発行して郵送してもらった後の本人確認で身分証が必要なパータンの2つを経験した。いずれにせよ身分証が必要になる。

運転免許証

運転免許センターに行けば再発行できるが、それには身分証が必要だった。

保険証

勤務先に再発行をお願いしたら1、2週間くらいで手元にきた。

すべてを再発行するまでの流れ

財布紛失により手元に身分証が一切無くなってしまったので、クレジットカード以外の再発行に苦労することになった。普段は持ち歩かないけど身分証として使いやすいものにパスポートがあるけど、自分の場合はちょうど期限が切れていて使えなかった。あとは当時自分は持っていなかったけどマイナンバーカードも持ち歩かない身分証となると思う。 当時の自分にとって、最初に再発行が可能なものは保険証のみだった。これが無かったら他にどうやって最初の身分証を得ることができたのかはよく分からない。なので保険証の再発行をお願いしつつ、キャッシュカード再発行の手続きを進めて保険証の到着を待った。 保険証が届いたら、次はそれを身分証として運転免許証を再発行した。これは運転免許センターに行けばわりと簡単。 顔写真と現住所が書かれた最強の身分証である運転免許証さえ手に入れば、キャッシュカードの再発行も進められる。ここはちょっと時間がかかったけど財布紛失からだいたい合計で1ヵ月くらいで完全に元の状態に戻った。

紛失した財布

紛失してから数か月くらいたってから、警察から連絡がきて紛失した財布が返ってきた。財布が届けられた先が出掛けた先だったので、駅からゲーセンまでの移動中かゲーセン内で紛失したのだろう。現金だけは綺麗になくなっていたが、カード類はそのまま返ってきた。まぁ返ってきたところでどれも無効化されてるわけだけど。 この経験をして以来、身分証とキャッシュカードは最低1つは自宅に置いておくよう気にしている……

ある式がある trait を実装しているかチェックするやつ

Rust は zero-cost abstractions を掲げているからか、map() や filter() のような色々な言語でよくある関数が元のコレクションの型ではなく Map や Filter のようにそれぞれ専用の型を持っているパターンがよくある。 それぞれの型が Iterator を実装しているので実装を書くときはコレクションの型のように扱えるけど、その値の具体的な型は複雑なことも多い。 async/await を使っていると Future や Stream でよくそういう状況になる。

正しく実装できていれば各 (部分) 式がどんな複雑な型を持っていても問題無いわけだけど、コンパイルエラーになってしまったときに悩むことになる。 コンパイラが「この部分式の型はこうなっていて、この trait を実装するよう要求されているけど満たしてないよ」と教えてくれるものの、その部分式がその trait を実装できていないのはもっと前のほうの実装ミスだったりする。 そういうときに「ここまででこの式/値はこういう trait を実装しているような型の値のはず」と assert を書きたくなってマクロを書いてみた。

https://github.com/eagletmt/assert_trait

std::dbg! や std::todo! といったマクロのように、開発中やデバッグ中には使われるけど最終的なコードには登場しないような使い方を想定している。 syn/quote crate めっちゃ便利……

長時間の I/O を走らせつつ定期的に短時間の I/O を走らせるやつ

長時間のタスクを走らせつつ定期的に短時間の I/O をしたいパターンがよくある。いわゆる heartbeat 的なことをやりたいときとか。 例えば長時間の外部コマンドを実行するとき、Go だと

package main

import (
    "fmt"
    "log"
    "os/exec"
    "time"
)

func main() {
    ch := make(chan error)
    go func() {
        ch <- exec.Command("sleep", "10").Run()
    }()

    ticker := time.NewTicker(2 * time.Second)
    defer ticker.Stop()

    var err error
L:
    for {
        select {
        case <-ticker.C:
            fmt.Println("Waiting...")
        case err = <-ch:
            break L
        }
    }
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("OK")
}

みたいに time.Ticker と select/channel で書きやすいんだけど、Rust だと stream だと考えて futures::stream::select() を使うと良さそうだった。 https://docs.rs/futures/0.3.7/futures/stream/fn.select.html

use futures::StreamExt as _;
use tokio::io::AsyncWriteExt as _;

#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
    let interval = tokio::time::interval(tokio::time::Duration::from_secs(2))
        .map(|_| futures::future::Either::Left(()));
    let long_running_command =
        futures::stream::once(tokio::process::Command::new("sleep").arg("10").status())
            .map(|result| futures::future::Either::Right(result));
    tokio::pin!(long_running_command);

    let mut stream = futures::stream::select(interval, long_running_command);
    let status = loop {
        let item = stream.next().await.unwrap();
        match item {
            futures::future::Either::Left(_) => {
                // Perform some lightweight I/O
                tokio::io::stdout().write_all(b"Waiting...\n").await?;
            }
            futures::future::Either::Right(result) => {
                break result;
            }
        }
    }?;
    if !status.success() {
        return Err(anyhow::anyhow!("command failed"));
    }
    println!("OK");
    Ok(())
}

十数年ぶりにメインマシンを Windows にした

給付金や中止になった色々なイベントの返金で気が大きくなった結果、Ryzen 9 3900X を使った新しいメインデスクトップマシンを組んでいた。構成はこんなかんじ。

  • CPU: AMD Ryzen 9 3900X
    • 3950X も考えたけど、コスパ重視で 3900X のほうを選んだ
    • AMD の CPU を買うのは今回が初めて
  • GPU: NVIDIA GeForce GTX 1660 Super (GG-GTX1660SP-E6GB/DF)
    • せっかく Windows をメインマシンにするならということで快適にゲームできそうなところを選んだ
    • 正直知見が薄いので GTX 1660Super に決めた後は適当に買いやすいのを選んだ
    • RTX はまぁ要らんやろ……
  • MB: ASRock X570 Extreme4
    • 自分の趣味により基本 ASRock
    • 余分な M.2 の口とかが欲しい前提で Steel Legend と Extreme4 で悩んで、前面にも Type-C を出せる Extreme4 にした
  • SSD: Samsung SSD 1TB 970 EVO Plus
    • M.2 な SSD を前提に選んだ
    • M.2 SSD で一定以上の容量だとここ一択な気はする

CPU クーラー

Ryzen のリテールクーラーがバンドルされてないモデルだと水冷が推奨されていることは事前に調べていた https://www.amd.com/ja/processors/3950x-thermal-solutions 。 とはいえ自分が買ったのはリテールクーラー (空冷) がバンドルされていたので、一度それで試してから必要そうだったら水冷のクーラーを買うことにした。 ffmpegエンコードしながら実際に試した結果、空冷でも危なさそうな温度にはならないものの、結構クロック数が下げられていたこととだいぶ騒音が激しいことから、結局簡易水冷のクーラーを買った。 実際に簡易水冷のクーラーで静かに同じ温度でもっとクロック数を上げることはできたけど、そこに値段相応の価値を感じるかは人それぞれっぽい。

Web 系開発者にとっての Windows での開発

自分が最後に Windows をメインマシンとして使っていたのは高校卒業までだったと思う。その頃は Web っぽいかんじではなかったけど、最終的に開発等には Cygwin を使っていた。大学に進学してからは MacBook (当時は Mac OS X Leopard だったかな) を使うようになり、その数年後から今年に至るまで Linux デスクトップをメインで使っていた。このへんの話は https://eagletmt.hateblo.jp/entry/2016/08/18/231349 にも書いた通り。今でも仕事では MacBook Pro も支給されつつも Linux デスクトップのほうを主に使っている。

そんな自分が今回 Windows をメインにしてみようと思った理由は2つあって、1つは Linux デスクトップの停滞でもう1つは WSL2 だった。 https://eagletmt.hateblo.jp/entry/2016/03/23/020117 を書いてから更に4年が経ったわけだけど、やはりここから大きく変化しておらず、計9年間くらいこの環境でやってるとさすがに飽きてきたというのがある。何年経っても同じように使えるのは安定しているということだし、Linux デスクトップ界隈の HiDPI 対応状況も良くなってはきているけど、まぁ飽きた。 そして WSL2 の体験が予想以上に良かった。メインマシンを Windows にしたのは今回が10+年ぶりだったけど、サブのマシン等でなんだかんだ Windows 環境にはずっと触れてきていて、その中で WSL1 にも触れてはいたけど、ディスク I/O の遅さが致命的であまり感動は無かった。しかし WSL2 ではかなり快適に動くようになって、メインマシンを Windows にしてみようと思えるくらいの期待を持てた。Web っぽいのとか CLI ツールとかはやはり Linux のほうが充実していて、そこがかなりシームレスに使えるというのは大きかった。我々が欲しかった (依存していた) のは UNIX ではなく Linux だった。 一方で IPv6 対応がまだだったり、D-Bus や X が一応動くけどややがんばりが必要だったり、改善されてほしい点はまだまだある。

Windows に戻ってみて

各種ドライバとか Steam のゲームが普通に動くのはなんだかんだ便利に感じる。Wine が異常によくできていたりはするんだけど、「がんばっても動かない」と「がんばると動く」の差が大きいように、それと同じくらい「がんばると動く」と「普通に動く」の差は大きい。

あとは Linux デスクトップで依存していたアレコレが動くかどうかだけど、

あたりでほぼ足りてる。

というわけで、個人的には Windows をメインマシンにして大きな困りはなく数週間くらい過ごせている。macOS の品質に疑問が出てきている [要出典] いま、Web 開発がメインでも Windows を選択するというのは普通にアリだと思う。