Open7

Rust 勉強メモ

tanishikingtanishiking
use std::fs::File;
use std::io::Read;

let mut binary = match File::open(filename) {
    Ok(file) => file,
    Err(_) => {
        process::exit(1);
    }
};

let mut buffer = vec![0u8; 0x200];
match binary.read(&mut buffer) {
  // ...
}
  • let mut buffer = vec![0u8; 0x200]; この buffer ってヒープに確保された値への参照じゃないのか〜? その通りで buffer は ヒープ上に確保された値へのファットポインタ、String とかもそうですね。
  • ここで mutable って言ってるのは ファットポインタが mutable ってことなのか? - そのようだ
  • なんかファットポインタへの参照を取るのって参照の参照をとることになって、ポインタ直接扱えばよいのにって思うけど、そうしないで参照を使ってownership管理されるのがRustスタイルって感じ?
    • 別にファットポインタをそのまま他の関数に渡す(move)してあげても良いんだけど
  • mutable buffer を不変参照にはできない
    • そもそも &buffer しようとすると error[E0596]: cannot borrow buffer as mutable, as it is not declared as mutable と怒られる。
  • 逆に immutable な値を可変参照にもできない
let buffer = vec![0u8; 10];
let mutable_ref = &mut buffer;
  • error[E0596]: cannot borrow buffer as mutable, as it is not declared as mutable なるほどね
  • それなら&mut と & をシンタックス上分けなくても良かった?と思ったけどmutable reference と immutable referenceの記号同じなのも分かりにくいか
tanishikingtanishiking

ownership

immutable reference は Copy セマンティクス

let s = String::from("hello");
let r1 = &s;
let r2 = r1; // copy
println!("{} and {}", r1, r2); // r1もr2も両方使える

mutable reference は Move セマンティクス

可変参照はデータを変更する排他的な権限をもつ。コピーできない。

let mut s = String::from("hello");
let r1 = &mut s;
let r2 = r1; // move
println!("{}", r1); // error

関数呼び出しは...?

Move セマンティクスじゃないらしい。どういう理屈?

fn bar(x: &mut i32) {
  *x += 1;
}

let mut a = 10;
let b = &mut a;
bar(b); // it doesn't move
*b += 10;

reborrow

haibane_tenshi's blog - Obscure Rust: reborrowing is a half-baked feature 前半わかりやすかったけど後半難しくて読んでない

fn main() {
    let mut num = 32_u32;

    let a = &mut num;
    // let b  = a; これだと move
    let b: &mut u32 = a; // &mut *a に自動変換
    *b += 1;
 // non-lexical lifetime のおかげで、ここで b が破棄?され、aにownershipが戻って来る?
    *a += 1; // なので ここで aが使える
}

num <-exclusive-borrow-- a <-exclusive-reborrow-- b こういう borrow 関係になり exclusive borrow を保ったまま、numへのmutable referenceをborrowできる。
The confused borrowing rules in rust - help - The Rust Programming Language Forum

こういうexclusive reborrow は 引数や、左辺が明示的に exclusive mutable reference を必要としている場合に自動的に reborrow への変換が行われるのだろうか?

tanishikingtanishiking

Dynamic Dispatch / impl Trait

impl Trait (parameter)

fn f(b1: impl Bar, b2: impl Bar) -> usize
// これは以下と同じ
fn f<B1: Bar, B2: Bar>(b1: B1, b2: B2) -> usize

Rustではgenericsはspecializationによって実装されているので、これは単相化され、関数呼び出しはstatic dispatchになる。

dyn Trait

dyn Trait を trait object と呼ぶ? どういうこと? dyn Trait で指定された引数として渡ってきたオブジェクトを Trait Object と呼ぶってこと?

dyn Trait を使った場合、引数の型は type erase されて、関数呼び出しは dynamic dispatch になる。単相化がないのでコードサイズが削減できる。

fn f(b: &dyn Bar) -> usize

Rust では unsized parameter を渡すことはできない
type erase された型はsizeがわからないので、Box<dyn Bar> や &dyn Bar などを使う必要がある

the indirection is necessary for another reason. These indirections are or contain wide pointers to the erased type, which consist of a pointer to the value, and a second pointer to a static vtable. The vtable in turn contains data such as the size of the value, a pointer to the value's destructor, pointers to methods of the Trait, and so on.
https://quinedot.github.io/rust-learning/dyn-trait-overview.html

また Box や &dyn を使った indirection は vtable を渡すためにも必要。これどういうこと? trait object を参照をとると勝手にvtableへの参照もついてくるってこと?
trait object そのものがvtableへの参照を持っていると思うので、別に indirection と vtable は関係ないのでは??? 単にサイズが不定な値を渡せないから indirection が必要。だけで良いのでは?

trait Trait1 {}
trait Trait2 {};
struct S(Box<dyn Trait1 + Trait2>);

こういう non-auto trait を複数もつ trait object は定義できない (sub trait を別途定義すれば良い)。

impl Trait (return position)

この場合は単相化にはならない。Java とか Scala だと、このfから返ってきた値の関数呼び出しは(de-virtulaizeされない限り)virtual dispatchになるわけだが、Rustだとどうなる?

fn f() -> impl Bar {
    Foo { ... } // Foo は Bar を impl している
}

この返り値の型は存在型 (∃ T. T: Trait) を表す、らしい。何???具体的に実行時にはどうなるんですか?

[Existential types in Rust | varkor’s blog]https://varkor.github.io/blog/2018/07/03/existential-types-in-rust.html) このブログにも書いているように、RPITが単に存在型を表しているなら以下のプログラムはコンパイル通ってほしいが、これはエラーになる

trait Trait {}
struct A;
struct B;
impl Trait for A {}
impl Trait for B {}

fn foo(pick_a: bool) -> impl Trait {
	if pick_a { A } else { B }
}
   |
13 |     if pick_a { A } else { B }
   |                 -          ^ expected `A`, found `B`
   |                 |
   |                 expected because of this

The problem is that the Rust compiler needs to know which (unquantified) type will be returned from the function. The existential type doesn’t “exist” at run-time — it needs to pick a specific unquantified type. (This makes returning an impl Trait just as efficient as any other type.)

つまり存在型といいつつも、実装の都合上?結局実行時に型を確定させるために?、一意に具体型が決まらないといけないらしい。つまり結局は返り値の impl Trait も単相化されそうだ。

ちなみにこのブログでは Argument Position の impl Trait も存在型だとみなすことができると主張している。

  • 直観主義論理においては ((∃ x. P(x)) → Q) ⇔ (∀ x. (P(x) → Q)) が成り立ち
  • Argument position Impl Trait (APIT) も存在型だとみなすと fn(∃ S. S: Foo) → T (fn foo(x: impl Foo) -> T に対応)
  • then ∀ S. (fn(S: Foo) -> T) and vise versa (fn foo<S: Foo>(x: S) -> T) 確かにね。

(ところでspecializationってどんな単相化された型を生成するんだ? このジェネリック関数を呼出している型Tに対してのみ単相化が実行される? じゃあspecialie時にプログラム全体or少なくともそのジェネリック関数を呼び出しているすべての関数が見えないといけないのでは)

tanishikingtanishiking

specialization の挙動

  • プログラム全体に現れる全ての型?そのgeneric関数/型を利用しているすべての型?に対して単相化が行われる?
  • 何れにしてもプログラム全体、もしくはプログラム全体でそのgeneric関数を利用している型が見れる必要がある。
  • 単相化はどのタイミングで行われる?MIRだかHIRだかの中間言語レベルでやられると思ってるけど、それはプログラム全体を見れる状態なんですか?
  • 勘だとクレートごとにひとつのHIR/MIRができて〜ってなる気がしてて、じゃあクレートを超えたジェネリック関数呼び出しはどうやってそれへの呼び出しがあることが分かる?

コンパイラの中身読んで調べてくれている 続くといいな日記 – Rust のジェネリック関数はどうコンパイルされるのか Rustコンパイラの中身を読んでいくRustヂカラがないのでありがたく読んでいく。

なお, Rust はライブラリを crate という単位で管理する.通常ビルドする時は,この crate ごとにスタティックライブラリのアーカイブである rlib ファイルを作る. rlib ファイルは,
オブジェクトファイル (.o)
LLVM のビットコード (
.bc.z)
Rust 独自のメタデータ (rust.metadata.bin)
を含む ar アーカイブになっている.

え、そうなの?なんかビルドのたびにソースコード引っ張ってきてビルドしてるように見えるけど、ていうかオブジェクトファイルもLLVM IRもだいたいプラットフォーム依存なので再利用性低くて使えないのでは?
MIRとか、ソースコードそのものが保存されてるんじゃないの?

ジェネリック関数が使われている部分を全て探し出して,
その中から具象型で使われてる部分を抽出し,どの具象型で使われてるかを洗い出し,
それぞれの具象型に対して単相的なコードを埋め込み,
使われてる部分のジェネリック関数の呼び出しを埋め込んだ単相的な関数の呼び出しにすげ替える.

プログラムの利用箇所を全部調べて、利用された引数の型についてすべて単相化するんですね。
じゃあプログラム全部見えてないといけなくない?それともなんかメタデータに利用された関数の型とかの情報が入ってる?(そんなローカルな情報も入れるのか?)

なので,ジェネリック関数が crate を超えて使われる場合,各 crate は単にリンクするようのオブジェクトファイルを提供するだけでなく,自身が持っているジェネリック関数の実装をメタデータとして渡してやる必要がある.

後ろの方はちゃんと理解できなかったけど、メタデータにパブリックなジェネリック関数の本体のMIRを埋め込み(何故???インターフェースだけで良くない?)
呼び出し側もジェネリック関数の呼び出に関する情報をメタデータに持ってるらしい。
なのでメタデータと合わせてMIRを見ていけば、プログラム全体のジェネリック関数の呼び出しに使われているすべての型が分かるということだろうか。

結局specializationはMIRレベルで実行されるという理解で良いのかな


なんか rlib のあたりが怪しい気がする。ソースコードそのもの or MIR を crate が持っていたら、単に MIR を全部見れば良くてメタデータ見る必要はないのでは?

tanishikingtanishiking

Rust に type based な overload がないのはなぜ?

↓かなり納得のいく内容だった(後半の具体例は少し微妙だと思ったが)
Why Not Type-Based Overloading in Rust? — Sympolymathesy, by Chris Krycho

fn do_something(foo: &Foo) { ... }
fn do_something(foo: &mut Foo) { ... }
fn do_something(foo: Foo) { ... }

という異なる型の引数を持つ同名の関数があったとき、これらの違いは引数に与える値を渡し方が違う(borrow / mutable borrow / move) だけなのに、プログラムの意味まで変わってしまうのはどうなのか?という話。

現状 fn do_something(foo: &Foo) { ... } に mutable reference を渡すことができるが、overload が可能になるとどうするんですかね? mutable reference なら fn do_something(foo: &mut Foo) { ... } が呼び出されるべきなのだろうか?