Pixel Pedals of Tomakomai

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

Rustでdocker build時に依存ライブラリの再ビルドを避ける

Rust で書かれたアプリをビルドするための docker image は以下で公開されている。

hub.docker.com

しかし、ここに出ているように COPY . . なんてやっていると、 src/main.rs を触るだけで毎回 cargo install 時に依存するライブラリをすべてダウンロードしてきてコンパイルしてしまい、恐ろしく効率が悪い。

これを避けるには、アプリをビルドする前に依存だけをビルドするレイヤを作っておき、その上に自分のアプリをビルドするレイヤを載せられるといい。しかし、残念なことに、現在の cargo には依存クレートだけをビルドする仕組みがない。じゃあどうするかというのが、以下のエントリに書かれている。

whitfin.io

要点だけ説明すると、まず cargo new --bin my_project とこれからビルドするプロジェクトと同名のダミーの空のプロジェクトを用意し、そこに Cargo.tomlCargo.lock のみを配置して cargo build --release する。その後 src/*.rs を本物に差し替えて、もう一度 cargo build --release すれば良い。ただし、自分で試してみたところなぜか 2 度目の cargo build が動いてくれなかったので、 touch src/main.rs をするなどの処理をおまけで入れておいた。

さて、手元での実行はこれで良いが、 Cloud Build: serverless CI/CD platform  |  Google Cloud を使うと、前回ビルド済のレイヤを参照できるわけではないのでキャッシュが効いてくれない。 --cache-from を使う方法もあるが、マルチステージビルドとだいぶ相性が悪い。というのも、ビルドに使ったイメージも破棄せず docker push しなければならないので。じゃあどうするかというのは、以下のエントリに書かれている。

qiita.com

kaniko を使えば、 cloudbuild.yaml は本当に大丈夫かいなってくらい恐ろしくシンプルになる。それでも動かしてみるときちんとキャッシュをする動きになっていて、初回実行が 18M52S かかる cargo build が2度目の実行が 1M25S で済んだ。すばらしい。

&Tがそのまま&dyn Traitとして使えるわけではない

トレイトオブジェクトを返す次の関数 fコンパイルできる。

trait X {}

fn f(x: &dyn X) -> &dyn X {
    x
}

ところが、次のように書くとコンパイルできない。

fn f<T: X + ?Sized>(x: &T) -> &dyn X {
    x
}
   Compiling playground v0.0.1 (/playground)
error[E0277]: the size for values of type `T` cannot be known at compilation time
 --> src/lib.rs:4:5
  |
3 | fn f<T: X + ?Sized>(x: &T) -> &dyn X {
  |      - this type parameter needs to be `std::marker::Sized`
4 |     x
  |     ^ doesn't have a size known at compile-time
  |
  = help: the trait `std::marker::Sized` is not implemented for `T`
  = note: to learn more, visit <https://doc.rust-lang.org/book/ch19-04-advanced-types.html#dynamically-sized-types-and-the-sized-trait>
  = note: required for the cast to the object type `dyn X`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0277`.
error: could not compile `playground`.

To learn more, run the command again with --verbose.

T はトレイト X を実装しているのだから、何もしなくても &dyn X なのではと思われるかもしれないが、この書き方をした f では x が直接返されるわけではなく、トレイトオブジェクトに変換される。トレイトオブジェクトについては Trait object types - The Rust Reference に記述がある。 Exploring Rust fat pointers でもトレイトオブジェクトの構造 (fat pointers) についてわかりやすく説明されている。

Each instance of a pointer to a trait object includes: a pointer to an instance of a type T that implements SomeTrait a virtual method table, often just called a vtable, which contains, for each method of SomeTrait and its supertraits that T implements, a pointer to T's implementation (i.e. a function pointer).

この変換時になぜ Sized が必要なのかは、以下の issue で説明されている。 [u8] のサイズの格納位置が &&dyn の場合で違うため、単純にポインタを流用してトレイトオブジェクトを作ることができないようだ。なるほど。

github.com

A legitimate example of why the Sized bound is needed is for e.g. Self = [u8]. You cannot cast &[u8] to &dyn Trait because there would be nowhere to store the length of the slice (&[u8] stores it in the same place where &dyn Trait puts its vtable).

ただ、この issue がなぜクローズされていないかはよくわからず。将来挙動が変わる見込みがあるんだろうか?

Rustの&mutのmoveとreborrow

&mutCopy trait を 実装していない 。よって、こちらは実行できない。

fn main() {
    let x: &mut i32 = &mut 0;
    {
        let y = x;
        println!("y: {}", y);
    }
    println!("x: {}", x);
}
   Compiling playground v0.0.1 (/playground)
error[E0382]: borrow of moved value: `x`
 --> src/main.rs:7:23
  |
2 |     let x: &mut i32 = &mut 0;
  |         - move occurs because `x` has type `&mut i32`, which does not implement the `Copy` trait
3 |     {
4 |         let y = x;
  |                 - value moved here
...
7 |     println!("x: {}", x);
  |                       ^ value borrowed here after move

error: aborting due to previous error

For more information about this error, try `rustc --explain E0382`.
error: could not compile `playground`.

To learn more, run the command again with --verbose.

しかし、なぜかこっちは実行できる。

fn main() {
    let x: &mut i32 = &mut 0;
    {
        let y: &mut i32 = x;
        println!("y: {}", y);
    }
    println!("x: {}", x);
}

ブロックを無くして、 xy を共存させると、コンパイルできなくなる。

fn main() {
    let x: &mut i32 = &mut 0;
    let y: &mut i32 = x;
    println!("x + y: {}", *x + *y);
}
   Compiling playground v0.0.1 (/playground)
error[E0503]: cannot use `*x` because it was mutably borrowed
 --> src/main.rs:4:27
  |
3 |     let y: &mut i32 = x;
  |                       - borrow of `*x` occurs here
4 |     println!("x + y: {}", *x + *y);
  |                           ^^   -- borrow later used here
  |                           |
  |                           use of borrowed `*x`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0503`.
error: could not compile `playground`.

To learn more, run the command again with --verbose.

cannot use *x because it was mutably borrowed という不穏なメッセージが出ている。これは、 y に代入しているものが &mut *x であることを暗に示している。実際、最初の例を以下のようにすると実行可能になる。

fn main() {
    let x: &mut i32 = &mut 0;
    {
        let y = &mut *x;
        println!("y: {}", y);
    }
    println!("x: {}", x);
}

この振る舞いを調べると、どうやら reborrow と言われている振る舞いであることがわかった。しかし、現時点で reborrow について明示的にドキュメントされてないらしく、以下の issue が上がっている。

github.com

もっとも、この挙動はレシーバが &mut self であるようなメソッドから、同様にレシーバが &mut self であるメソッドを複数回呼ぶだけで自然に発生する(ただ、メソッドのレシーバは auto dereference が効くので、特殊な気もするが)もので、特筆しなくても常識だろうということなのかもしれない。とはいえ、 Copy じゃない割に move しているようにも見えなくて腹落ちしてなかったので、そこがはっきりしたのは調べた甲斐はあった。

Surface Book 3を手にするまでの2ヶ月の苦闘の記録

Surface Book 2 を買ってから 2年半が経過し、今回 Surface Book 3 を購入した。日本でも販売されることを知らずに US のマイクロソフトストアで購入したのだが、今回実機を手にするまでにめちゃめちゃ苦労したので、記録を残しておく。 US キーボードが欲しかったので、まあ、苦労した意味は0ではないかなと思っている。

5/7 予約

Surface Book 2 がとても気に入っていたので、 Surface Book 3 の発表はずっと待っていた。 発表された ことを知り、即予約をした。クレジットカードの認証がうまくいかなかったり、2重で注文してしまったり(!)と言ったトラブルで2回サポートとチャットしたが、サポートがうまく対処してくれ、この時点では特に大きな問題は起きなかった。

5/23 問題その1. Mailout canceled

予定通りマイクロソフトから商品が発送され、 5/22 には転送業者である Planet Express の倉庫に到着した。本当は昔から愛用している 1worldshopping.com を使いたかったのだが、 事務所の契約期限が切れて閉鎖 されてしまったので、急遽見つけたのが Planet Express だった。結論から言えば、 Planet Express はきちんとサポートしてくれるし、よいサービスである。

倉庫に荷物が届いたということで、早速日本への配送手続きを済ませてワクワクしながら寝たのだが、翌日、発送をキャンセルするというメールを受け取って青ざめた。全く知らなかったのだが、 US から日本へ輸入をする際、金額が $2,500 を超えると Electronic Export Information という物が必要になるようだ。 1worldshopping は勝手にやってくれていたのだが、 Planet Express は別途手続きが必要らしい。手続きが必要と言ってもなんのことやらさっぱりだし、品物の金額が金額なだけに、US国内で破棄するという最悪の事態まで考えて胃が痛くなった。

しかし、蓋を開けてみれば話は簡単で、追加料金 $40 を支払うことで、特に追加の書類などは用意せずとも、 Planet Express 側で勝手にやってもらえた。まあ、さすがに $40 は高いと思うので、今度からは高額なものを扱うときは別の業者を探すかもしれない。ともあれ、三日後の 5/26 には無事に日本から発送された。

5/30 問題その2. 初期不良

コロナの影響で遅れることを覚悟したのだが、そこはさすが FedEx 。めちゃくちゃ高いだけあって、消費税が高過ぎて一瞬だけ税関で止まったが、電話で FedEx にクレジット番号を教えたら決済してくれて、 4 日後の 5/30 には日本郵政経由で Surface Book 3 を受け取ることができた。ここですべてが無事終わったと思っていたが、実はこの日が苦闘の始まりに過ぎなかったのだ。

開封してセットアップするものの、残念ながら全く感動はない。そりゃあそうだ、 Surface Book 2 と中身以外は全く同じなのだから。まあ、もともと気に入っているもののリプレースなので、そんなもんだろうと思いながら淡々とセットアップをしていたのだが、途中でおかしなことに気がつく。・・・画面に頻繁に横線上のノイズが入っている?? 初めて気がついたのは Windows Update を適用して再起動をしたとき。その時は気のせいだと思っていたが、時間が経てば経つほどどんどんノイズの頻度は上がっていく。これはヤバい、と思い始めた。

なんとか解決できないかといろいろなことをした。ドライバのアップデートをしたり、オンボードIntel Iris Pro Graphics を無効にして使ってみたり。一時期、改善したような気分になったこともあった。しかし、忘れた頃に高負荷になるとノイズが現れる。高負荷にならないようにだましだまし使えば、この個体とも上手に付き合えるのではないかとも考えてみた。しかし、これだけ高額なものを買って妥協するのは、流石に辛すぎる。セーフモードですら発生していたので、ドライバのアップデートで将来治ることも期待できない。

セーフモードでのノイズの様子

4日後の 6/2 に、諦めてサポートに連絡した。この時点で 海外購入の Surface Book が故障したときの涙の物語 のエントリは読んでいたので、日本から直接 Surface Book 3 を送りつけて交換してもらうことは無理だろうなと把握していた。最終手段として、アメリカに移住した元同僚に連絡を取り、助けを求めることにする。幸い、米国でコロナやデモが大変な中、快く手伝ってもらえた。彼がいなければ今回は完全に詰んでいたので、どれだけ感謝してもしきれない。

6/3 に FedEx で US に発送し 6/5 には元同僚の家に到着した。Surface Book 3 のリプレースは、マイクロソフトに発送ラベルを発行してもらい、それを使ってマイクロソフトのサポートセンターへ送付することで交換してもらえる仕組みになっている。この発送ラベルの住所が US のものじゃないと受け取ってもらえないそうだ。戻りの通関手続きなどを考えれば、そりゃあそうであろう。ここでラベルの発行処理に手間取ってサポートへ3~4度メールとチャットで連絡する羽目になったが、翌日の 6/5 には UPS のラベルを発行してもらい、 6/7 に元同僚がマイクロソフトサポートに向けて発送してくれた。

6/13 問題その3. 初期不良のある Surface Book 3が行方不明

UPSの追跡番号によれば、 6/10 にはマイクロソフトサポートセンターに到着したことになっているのだが、マイクロソフトの管理画面で見ることができる受注ステータスが一向に変わらない。 6/13 にサポートへ連絡したところ、コロナの影響で遅れているので4営業日待ってくれと言われた。しかし、一向に状況が変わらず 6/19 にサポートへ連絡したところ、状況がわからないので調べるから待ってくれという返答。とても嫌な予感がする。そして、残念ながらその後も連絡が来なかったので 6/22 に連絡をしたところ、Surface Book 3 はどうなっているかわからないという返答。おいおい、マジかよ・・・。そこは流石に高額な品だったこともあり、責任者らしき人が出てきて 24 時間以内に連絡するので待てと言われた。その後、 23 時間程度経ったところで返品の注文がキャンセルされてなかったことにされたので、このままバックレられるかとビビったのだけど、その 50 分後( 24 時間経過の 10 分前) に新しいオーダーが作成されて、今送ったから5日以内に代替機が届くよと連絡が来た。元同僚宅に代替機が届いたのは、 6/25 のこと。ここでも FedEx は速かった。

6/28 問題その4. 謎の小包

さて、あとは日本に発送してもらうだけだと思っていたら、マイクロソフトからまたメールが来た。どうやら UPS 経由でもうひとつ何かを元同僚宅へ輸送中らしい。おいおい、まさか重複して配送してないだろな・・・と思ってサポートに連絡すると、不明とのこと。2台到着したらやばいなあと思っていたのだけど、元同僚宅に到着したのは最初に送った初期不良Surface Book 3 の電源コードだけが送られてきた。どうやら、修理のときには電源ケーブルは送らなくて良かったらしい。それならそうとサポートに問い合わせた時点で言って欲しい・・・。

代替機の Surface Book 3 は元同僚に念の為動作チェックまでしてもらい、6/28にFedExに日本宛に郵送してもらった。そして、 7/1 、本日、無事に受け取ることができた。最初の注文から2ヶ月弱、長い戦いだった・・・。日本に到着後、セイノースーパーエクスプレスが配達日を守らないなんてことを最後の最後にやってくれたのだけど、ここまでの工程を考えれば誤差の範囲だろう。

感想

わかっているつもりではあったけど、やはり個人輸入のリスクはとてもでかいなあと思った。今回手伝ってくれるUS在住の人が居なかったら完全に終わっていたので。 US在住の知り合いがもっと欲しい。

後、USマイクロソフトのサポートは、毎回思うのだけどサポートを受けにくいなと思う。大きい会社なので仕方がないのだろうけど。チャットでサポートを捕まえるのは比較的簡単なのだけど、その後メールに移ってしまうと数日に一回しかレスがもらえなくなる。そこでチャットで別のサポートを捕まえるのだけど、引き継ぎの仕組みが貧弱なようで、情報の齟齬が発生してトラブルが大きくなる。今回も合計で10回以上サポートに連絡をしており、人によってサポートの質も大きく違うのもあり、本当に大変だった。

冒頭に書いたとおり、 Surface Book 3 は基本的に Surface Book 2 と同じなので、特に感想は書かない。 Surface Book 2 の時点ですでに完成された機体で、今でもめちゃめちゃ気に入っているので、後はこれ以上故障が発生しないことを祈るばかりである。

FutureとUnpin

FutureUnpin が必要なときは pin してねって書いてある。

Pinning - Asynchronous Programming in Rust

To use a Future or Stream that isn't Unpin with a function that requires Unpin types, you'll first have to pin the value

BoxUnpin なので、 Pin する必要ないのではと一瞬考える。

boxed.rs.html -- source

impl<T: ?Sized> Unpin for Box {}

しかし、残念ながらそれでは駄目で、理由は Box<F>Future になってくれないから。

boxed.rs.html -- source

impl<F: ?Sized + Future + Unpin> Future for Box<F>

なぜ Box<F>Future にするのに F: Unpin がいるのかというと、 poll を呼び出すにはポインタ先の値を Pin する必要があるが、 Unpin じゃなければそれはできないから。

    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        F::poll(Pin::new(&mut *self), cx)
    }

実際、 Pin::new には Unpin の制限が入っている。

pin.rs.html -- source

impl<P: Deref<Target: Unpin>> Pin

pub fn new(pointer: P) -> Pin

Box して Unpin にしてしまえば、さらにそれを Pin することで Future にできる。 DerefFuture を参照してるだけで十分なのがポイント。

future.rs.html -- source

impl<P> Future for Pin<P>
where
    P: Unpin + ops::DerefMut<Target: Future>

元々 Pin されてるので、内側の poll を呼ぶのに苦労はない。

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        Pin::get_mut(self).as_mut().poll(cx)
    }

DerefMut であれば as_mut が使えるので、ここで &mut (dyn Future) に変換でき、 poll が呼べる。

pin.rs.html -- source

pub fn as_mut(&mut self) -> Pin<&mut P::Target>

めでたしめでたし。

AVL木

確か、いい感じに深くなりすぎない木だよね。回転?なにそれ?みたいな状態から実装し始めたら、面倒で死ぬかと思った。

AVL Tree by Java -- これで分かったAVL木 とか AVL木の回転(要素の挿入や削除のしかた) - Qiita とかを参考に実装した。2週間くらい前から2回ほどチャレンジしてギブアップしていて、本日、腰を据えて三度目の正直。コードを整形する気力が残ってないので、そのまま貼り付け。

type 'a btree =
| Node of int * 'a * 'a btree * 'a btree
| Leaf
;;

let rotate_right t =
  match t with
  | Leaf -> Leaf
  | Node (bx, x, Node (by, y, ty1, ty2), tx2) ->
      let hty1 = by in
      let hty2 = 0 in
      let hy = max hty1 hty2 + 1 in
      let htx2 = hy - bx in
      let bx' = hty2 - htx2 in
      let hx' = max hty2 htx2 + 1 in
      let by' = hty1 - hx' in
      Node(by', y, ty1, Node(bx', x, ty2, tx2))
  | _ -> failwith "can't rotate right"
;;

let rotate_left t =
  match t with
  | Leaf -> Leaf
  | Node (bx, x, tx1, Node (by, y, ty1, ty2)) ->
      let hty1 = by in
      let hty2 = 0 in
      let hy = max hty1 hty2 + 1 in
      let htx1 = hy + bx in
      let bx' = htx1 - hty1 in
      let hx' = max htx1 hty1 + 1 in
      let by' = hx' - hty2 in
      Node(by', y, Node(bx', x, tx1, ty1), ty2)
  | _ -> failwith "can't rotate left"
;;

let rotate_left_right t =
  match t with
  | Node (bx, x, Node(by, y, ty1, ty2), tx2) ->
      rotate_right (Node (bx, x, Node (by, y, ty1, rotate_left ty2), tx2))
  | _ -> failwith "can't rotate left right"
;;

let rotate_right_left t =
  match t with
  | Node (bx, x, tx1, Node(by, y, ty1, ty2)) ->
      rotate_left (Node (bx, x, tx1, Node (by, y, rotate_right ty1, ty2)))
  | _ -> failwith "can'trotate right left"
;;

let bias t =
  match t with
  | Leaf -> 0
  | Node (bx, _, _, _) -> bx

let insert_avl t a =
  let rec insert_avl' t a =
    match t with
    | Leaf -> (Node (0, a, Leaf, Leaf), true)
    | Node (bx, x, tx1, tx2) ->
        if a < x then let (tx1', is_grown) = insert_avl' tx1 a in
                      let bx' = bx + if is_grown then 1 else 0 in
                      let t' = Node (bx', x, tx1', tx2) in
                      if is_grown then if bx' == 2 then if bias tx2 == -1 then (rotate_left_right t', false)
                                                                          else (rotate_right t', false)
                                                   else (t', bx' != 0)
                                  else (t', false)
                 else let (tx2', is_grown) = insert_avl' tx2 a in
                      let bx' = bx - if is_grown then 1 else 0 in
                      let t' = Node (bx', x, tx1, tx2') in
                      if is_grown then if bx' == -2 then if bias tx1 == 1 then (rotate_right_left t', false)
                                                                          else (rotate_left t', false)
                                                    else (t', bx' != 0)
                                  else (t', false)
  in let (t', _) = insert_avl' t a in t'
;;

実装してて思ったんだけど、左回転して右回転・・・のような話は、どこかで聞いたことがある気がする。大昔になんかの本で読んだのだろうか。

rustでdiscordのbotを作ってみたくて下調べ(4)

rustでdiscordのbotを作ってみたくて下調べ(3) - 北海道苫小牧市出身の初老PGが書くブログ で serenity を紹介したが、最近 async版 の開発が進んでいる。試してみたが、まともに動くようだ。ベンチマークなど取っていないので、 async にする価値があるのかはよくわかっていないが、 Rust の async/await を使ってなにか作ってみたいというのがもともとのモチベーションなので、これを使ってみることにする。まだ PR の段階でマージされるまではしばらくかかりそうなので、利用したければ Cargo.toml へ以下のように書く。

[dependencies.serenity]
git = "https://github.com/Lakelezz/serenity.git"
branch = "await"

触ってみたところ、早速不具合を2つ見つけたので PR を送っておいた。

ところで、前回 tokio-tungstenite を使って自前実装を作っていると書いたが、実は後継(ただのフォーク?)の async_tungstenite - Rust なんてものも開発されているようだ。試してはいないが tokio だけではなく async-std にも対応していそうなので、 WebSocket の async/await 実装としてはこちらも検討するといいだろう。