Try   HackMD

Crust of Rust : Dispatch and Fat Pointers

直播錄影

  • 主機資訊
    ​​​​wilson@wilson-HP-Pavilion-Plus-Laptop-14-eh0xxx ~/CrustOfRust> neofetch --stdout
    ​​​​wilson@wilson-HP-Pavilion-Plus-Laptop-14-eh0xxx 
    ​​​​----------------------------------------------- 
    ​​​​OS: Ubuntu 22.04.3 LTS x86_64 
    ​​​​Host: HP Pavilion Plus Laptop 14-eh0xxx 
    ​​​​Kernel: 6.2.0-37-generic 
    ​​​​Uptime: 22 mins 
    ​​​​Packages: 2367 (dpkg), 11 (snap) 
    ​​​​Shell: bash 5.1.16 
    ​​​​Resolution: 2880x1800 
    ​​​​DE: GNOME 42.9 
    ​​​​WM: Mutter 
    ​​​​WM Theme: Adwaita 
    ​​​​Theme: Yaru-dark [GTK2/3] 
    ​​​​Icons: Yaru [GTK2/3] 
    ​​​​Terminal: gnome-terminal 
    ​​​​CPU: 12th Gen Intel i5-12500H (16) @ 4.500GHz 
    ​​​​GPU: Intel Alder Lake-P 
    ​​​​Memory: 1204MiB / 15695MiB 
    
  • Rust 編譯器版本 :
    ​​​​wilson@wilson-HP-Pavilion-Plus-Laptop-14-eh0xxx ~/CrustOfRust> rustc --version
    ​​​​rustc 1.70.0 (90c541806 2023-05-31) (built from a source tarball)
    

Introduction

0:00:00

In this episode of Crust of Rust, we go over static and dynamic dispatch in Rust by diving deep into generics, monomorphization, and trait objects. As part of that, we also discuss what exactly the Sized trait is, what it's for, and how it interacts with Dynamically Sized traits.

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Performance of Code Using Generics
Rust accomplishes this by performing monomorphization of the code using generics at compile time. Monomorphization is the process of turning generic code into specific code by filling in the concrete types that are used when compiled. In this process, the compiler does the opposite of the steps we used to create the generic function in Listing 10-5: the compiler looks at all the places where generic code is called and generates code for the concrete types the generic code is called with

Monomorphization

0:03:08

開始建置 Rust 專案 :

$ cargo new --lib eksempel
$ cd eksempel
$ vim src/main.rs

先看到一個簡單的例子 :

pub fn strlen(s: impl AsRef<str>) -> usize { s.as_ref().len() } pub fn strlen2<S>(s: S) -> usize where S: AsRef<str> { s.as_ref().len() } pub fn foo() { strlen("hello world"); // &'static str strlen(String::from("hei verden")); // String }

兩個函式做相同的事,但不完全等價。兩者皆為泛型函式,且都接收可以轉成參考的參數。

編譯器會為兩種不同輸入型別的 strlen 產生程式碼 copys :

pub fn strlen_refstr(s: &str) -> usize { s.len() } pub fn strlen_string(s: String) -> usize { s.len() }

這種情況不只發生在函式,也會發生在型別上面,例如泛型於 key 和 value 的 HashMap,編譯器會給你完整的結構、以及你使用到的方法的 copys。編譯器只會根據使用到的參數型別來產生 copy,並不會為所有可能的型別來產生 copy,這也是為什麼你編譯完之後並移交給別人作為函式庫使用很困難。

假設現在分別傳入 key 型別為 String 型別和 key 為 number 型別到 HashMap。實際上,當編譯器產生 HashMap 的每個方法的程式碼時,它會 inline 每個 key 型別的 hash function的定義。因此,使用 String 的 HashMap 程式碼將直接將 String 的 hash 程式碼放入 HashMap 程式碼中,並且可以根據該型別是 String 的事實最佳化該程式碼。同樣地,對於 number 之類的東西,它可能能夠完全跳過 hash,因為它意識到一旦對所有泛型進行了 monomorphizes,實際上 hash 只是數字的值。這可能實際上並不準確,但你可以理解,編譯器可以看到用於特定型別的具體程式碼,進而更好地進行最佳化

編譯器會為泛型或泛函產生多個 copys 的程式碼也是有它的缺點,除了二進位檔比較大,還有程式可能變得比較沒有效率的 side effect,因為多個 copys 意味著 instruction cache 可能無法利用到 locality 的好處。

Q: Is that one of the primary reasons Rust binaries are larger than C ones?
A : Rust 的二進位檔通常包含更多靜態編譯的內容,這可能是原因之一,還有一部分原因是 monomorphization,另一個非常重要的原因是,Rust 通常內建 debug symbols。如果你不從二進位檔中剝除它們,最終會得到一個實際機器碼很少但具有大量 debug symbols 的二進位檔,因此你可能希望嘗試剝除二進位檔的 debug symbols。

Q : Will the compiler try to inline generated functions ?
A : 這就是為什麼 monomorphization 很好的原因之一,因為編譯器不僅僅是 inline string 或 str,它還可以選擇 inline 函式,並基於此進行最佳化。

pub fn bool_then<T>(b: bool, f: impl FnOnce() -> T) -> Option<T> { if b { Some(f()) } else { None } }

Q : big reason for larger size is stdlib
A : 這並不完全正確,因為只有你實際使用的標準函式庫部分最終才會放入你的二進位檔中。不過,如果你從標準函式庫中使用了大量的泛型,這確實會增加二進位檔的大小。

Q : How big of a cost is duplication of methods? Is it negligible?
A : 基本上沒有。Jon 認為實際產生多個函式並不那麼重要,但重要的是如果產生函式的許多 copys,則必須將它們中的每一個都編譯為機器碼,這會減慢編譯過程。

Q : wouldn't strlen_string take &String rather than just String by value?
A : 實際上,它可以接受任一種,兩者都 work,這也是採用 AsRef<str> 的原因之一。

Q : How do dynamic libraries handle generics?
A : 它們不這樣做。這就是為什麼動態連結 Rust 函式庫,甚至只是以二進位形式發佈 Rust 函式庫具有挑戰性的原因之一。而且,如果你希望在 Rust 中使用一個 C 的動態連結函式庫,而這個函式庫有很多像是外部函式之類的功能,那麼這些函式是不能使用泛型的。

(Static) Dispatch

0:16:13

使用例子來說明 Static Dispatch :

pub trait Hei { fn hei(&self); } impl Hei for &str { fn hei(&self) { println!("hei {}", self); } } pub fn foo() { "J".hei(); // 編譯器首先檢查 "J" 有哪些可用的方法, // 它不是直接去找一個叫 hei 的方法, // 而是去尋找在 scope 內的 trait, // 發現 &str 實作了 hei 方法,於是就呼叫了該方法。 } // 泛化版本 pub fn bar(h: impl Hei) // impl Hei 是泛化的語法糖 { h.hei(); // 當編譯器必須為此產生機器碼時, // 它需要以某種方式呼叫此方法, // 但它實際上並不知道 h 的型別。 // 這就是 monomorphization 發揮作用的地方。 } // 泛化版本等價程式碼 pub fn bar2<H: Hei>(h: H) { h.hei(); } // 編譯器產生的程式碼 // 這就是 "Static Dispatch",在編譯時期,編譯器知道真正的型別是什麼。 pub fn bar_str(h: &str) { h.hei(); // 當編譯器嘗試呼叫 hei 方法時,它就是知道那就是 Line 8 的方法。 // 這基本上變成了在組合語言程式碼中呼叫 Line 8 的函式呼叫, // 而該函式位於記憶體中的某個已知位置。 }

如果你不想編譯器產生一大堆的 copys,你可以使用 Dynamically Sized Types

接著用 vector 來說明我們的需求 :

pub trait Hei { fn hei(&self); } impl Hei for &str { fn hei(&self) { println!("hei {}", self); } } impl Hei for String { fn hei(&self) { println!("hei {}", self); } } // 一般 vector 的元素都是相同型別 pub fn foo() { for h in vec!["J", "Jon"] { h.hei(); // ok } } pub fn foo2() { bar(&["W", "Wilson"]); // ok bar(&[String::from("W"), String::from("Wilson")]); // ok bar(&["W", String::from("Wilson")]); // not ok // 我們希望 Line 45 是 ok 的, // 我們應該要能夠創建一個具有這種行為的 vector,而不關心具體型別。 // 這就是我們進入 dynamic dispatch 和 trait objects 領域的地方。 } // 現在我們在意的是元素有實作 Hei trait, // 而不是元素要有相同的型別。 pub fn bar(s: &[impl Hei]) { for h in s { h.hei() } }

Trait Objects

0:22:49

Trait Objects 章節談論了將不同具體型別的事物視為同一型別的能力。該章節使用的例子是 gui,你可能有很多可繪製的東西,它們實作了 Draw trait。你可能想要有一個 draw 函式,它只接受所有要繪製的東西的 list 或 iterator。其中一些可能是按鈕,有些可能是圖片,有些可能是文字方塊,但對於 draw 函式來說,這並不重要。它只關心它得到一個可繪製的東西的 iterator,這在某種程度上與前面的例子相似,前面例子只想要一個可以呼叫 hei 方法的東西的 slice。

pub fn bar(s: &[impl Hei]) { for h in s { h.hei() } } // 等價程式碼 pub fn bar<H: Hei>(s: &[H]) // 從 <H: Hei> 的線索可以得知,bar 只泛化於"一種"型別。 // pub fn bar<H: Hei, H2: Hei>(s: &[H]) // 傳入參數的 slice 也沒地方可以擺 H2。 // pub fn bar(s: &[Hei]) // 我們真正想要的是這種。 { for h in s { h.hei() } }

嘗試編譯以下程式碼 :

pub trait Hei { fn hei(&self); } impl Hei for &str { fn hei(&self) { println!("hei {}", self); } } impl Hei for String { fn hei(&self) { println!("hei {}", self); } } // 加 dyn 是為了先 trait objects must include the `dyn` keyword 的錯誤 : pub fn bar(s: &[dyn Hei]) { for h in s { h.hei() } }

得到以下編譯錯誤 :

$ cargo check
...
error[E0277]: the size for values of type `dyn Hei` cannot be known at compilation time
  --> src\lib.rs:22:16
   |
22 | pub fn bar(s: &[dyn Hei])
   |                ^^^^^^^^^ doesn't have a size known at compile-time
   |
   = help: the trait `Sized` is not implemented for `dyn Hei`
   = note: slice and array elements must have `Sized` type

接下來的直播將談到我們收到這個錯誤的原因、它的含義、修復方法、Sized 的含義以及它與 dynamic dispatch 和 trait 物件的關係。

The Sized Trait

0:27:13

先看到 Sized trait,它沒有任何的方法,文件寫道 :

Types with a constant size known at compile time.

大部份的型別都是 Sized,即便是以下結構,它也有一個 implicit 的 bound,要求該型別是 Sized :

struct Foo<T>(T);

回到我們的 strlen 的例子 :

pub fn strlen<S: AsRef<str>>(s: S) -> usize { s.as_ref().len() }

當你要呼叫 strlen,你必須傳入某個參數,strlen 函式接收到的參數必須占用 stack 上的空間或者必須被傳遞到一個暫存器中。這意味著編譯器必須確保產生的組合語言中有為該參數分配所需空間的程式碼,這要求編譯器知道該參數的大小。相似地,如果我們的回傳值也是泛型,編譯器也要知道該配置多少 stack 的記憶體給回傳值使用。

觀察 strlen 的具體實作 :

pub fn strlen_str(s: String) -> usize { s.len() }

String 的底層有兩個 numbers,分別是儲存字串的長度以及配置到的記憶體空間,以及一個指標指向位於 heap 的首字元。所以編譯器確實知道 String 的 Sized 是 3 個 usize。

回頭看到剛剛的編譯錯誤 :

error[E0277]: the size for values of type `dyn Hei` cannot be known at compilation time --> src\lib.rs:22:16 | 22 | pub fn bar(s: &[dyn Hei]) | ^^^^^^^^^ doesn't have a size known at compile-time | = help: the trait `Sized` is not implemented for `dyn Hei` = note: slice and array elements must have `Sized` type

我們只有告訴編譯器傳入參數應該要實作 Hei trait,我們並沒有告訴它這個參數的大小是什麼,而且傳入參數可能是 String, str 或其他實作 Hei trait 的型別,而這些型別的大小也都不一樣 (String: 2 個 numbers + 1 個指標, str: 1 個 number + 1 個指標)。

傳入參數的型別 &[dyn Hei] 沒有 Sized。slice 實際上只是一段連續的記憶體,每個區塊的大小相同,它是一個陣列,其中所有元素的大小都相同,但如果我們不知道元素的大小,我們就不能保證元素的大小相同。

當你問我「給我第四個元素」時,通常在一個陣列中,因為所有元素的大小相同,第四個元素就是指向起始點的指標加上一個元素大小的三倍。這是一種指標算術,用來獲得第四個元素。但如果它們的大小不同,你無法產生這樣的程式碼。這就是為什麼 Line 8 說「slice 和陣列的元素必須具有大小型別」,因為否則,該型別就毫無意義。

我們該如何解決這個問題呢?我們真的希望能夠做到這一點。另外,並不是只有陣列會遇到 Sized 未給定的問題 :

pub fn strlen_dyn(s: dyn AsRef<str>) -> usize { s.len() }

上面的程式碼編譯後也會得到跟剛剛一樣的錯誤 :

$ cargo check
    Checking eksempel v0.1.0 (C:\Users\user\wilson\CrustOfRust\eksempel)
error[E0277]: the size for values of type `(dyn AsRef<str> + 'static)` cannot be known at compilation time
 --> src\lib.rs:1:16
  |
1 | pub fn strlen1(s: dyn AsRef<str>) -> usize
  |                ^ doesn't have a size known at compile-time
  |
  = help: the trait `Sized` is not implemented for `(dyn AsRef<str> + 'static)`
help: you can use `impl Trait` as the argument type
  |
1 | pub fn strlen1(s: impl AsRef<str>) -> usize
  |                   ~~~~
help: function arguments must have a statically known size, borrowed types always have a known size

Q : Can you show us implementing Sized?
A : Sized 對於任何可以實作它的型別都會自動實作。因此,如果你確實喜歡沒有欄位的 foo 結構,那麼它就是 Sized :

struct Foo;

如果你為它添加欄位,且全部欄位都是 Sized,foo 就會是 Sized :

struct Foo { s: String, f: bool };

即便你加了泛型參數, foo 仍是 Sized :

struct Foo<T> { s: String, f: bool, t: T }; // 每個 trait bound 都有一個 implicit 的要求,即泛型參數是 Sized。 struct Foo<T: Sized> { s: String, f: bool, t: T };

Q : Is the issue stack SIZE? Isn't it just that you need to have statically known stack frames in order for you to compile efficient function code?
A : 問題不是 stack 是不是夠傳入參數用,而是當你呼叫該函式時,你需要知道為該函式的 stack 變數分配多少空間,當你必須為回傳值分配空間時,該空間應有多大?基本上,這就需要編譯器知道這些空間的大小,它才能知道 stack 指標要移動多少。雖然有一些巧妙的方法可以做到這一點,但基本上,為了產生高效的程式碼,編譯器必須知道這些東西的大小。

Q : What things aren't Sized?
A : 例子如下 :

pub trait Hei { fn hei(&self); } impl Hei for &str { fn hei(&self) { println!("hei {}", self); } } impl Hei for String { fn hei(&self) { println!("hei {}", self); } } // not Sized pub fn foo(h: dyn Hei) {} // Hei 是一個 trait,很多型別實作了這個 trait, // 這些有實作 trait 的型別之間的大小不同。 // Sized pub fn foo2<H: Hei>(h: H) {} // 因為我們為每個具體型別取得了 foo 的 copy 且對於每個 copy,型別的大小都是已知的。 // 因此,如果我們在這裡使用 static dispatch 來進行 monomorphization,那就沒問題了。

其它非 Sized 的例子如下 :

// not Sized struct Foo { s: str }; // str 不是 Sized,因為一個字串的大小可以是任意的, // 你考慮的不是字串的指標,而是字串本身的大小可以是任意的。 // not Sized struct Foo2 { s: [u8] }; // 你不知道沒有參考的 slice 裡面有幾個元素,所以也不是 Sized。

Sizing Unsized Types

0:39:34

我們希望能夠擁有一個不被 monomorphize 的方法,但傳入參數是 not Sized,我們該怎麼做 ?

pub fn strlen_dyn(s: dyn AsRef<str>) -> usize { s.len() }

這裡的技巧是使傳入參數的型別始終是 Sized,並且讓這個型別是 Sized 的型別指向型別是 not Sized 的型別。例如我們可以用到參考,因為參考始終具有相同的 Sized (或者在某些情況下是兩個指標的大小 (fat pointer),我們稍後會談到這一點) :

-pub fn strlen_dyn(s: dyn AsRef<str>) -> usize +pub fn strlen_dyn(s: &dyn AsRef<str>) -> usize { s.as_ref().len() }

編譯器始終知道參考的占用的空間多大,不論後面跟著的是什麼型別,因為這只是一個指標。

如果你有一個 Box,也可以讓傳入參數的型別是 Sized :

-pub fn strlen_dyn(s: &dyn AsRef<str>) -> usize +pub fn strlen_dyn(s: Box<dyn AsRef<str>>) -> usize { + // Jon 後面才從呼叫一次 as_ref() 改為呼叫兩次 as_ref() + // 我自己先加 + // 因為 Box 也有 as_ref() 方法 + // 所以需要呼叫兩次 as_ref() s.as_ref().as_ref().len() }

Box 基本上有類似於指標的欄位,Box 有著 ?Sized bound :

// ? 表示 T 可以為 Sized 或者不為 Sized // struct Box<T: ?Sized> {/* ... */} // 這就是為什麼你可以創建一個 Box,裡面裝的東西是 not Sized pub fn strlen_dyn(s: Box<dyn AsRef<str>>) -> usize { s.as_ref().as_ref().len() } pub fn main() { // 1. 最初創建 Box 的時候會進行記憶體配置, // 該配置的大小就是要放入 Box 內的任何內容的大小。 // 2. Box 必須傳入具體型別,且該型別是 Sized。 let x = Box::new(String::from("hello")); let y: Box<dyn AsRef<str>> = x; strlen_dyn(y); }

Q : can you just put Box<dyn Asref<str>> directly on x?
A : 可以,如下:

pub fn main() { let x: Box<dyn AsRef<str>> = Box::new(String::from("hello")); strlen_dyn(x); }

Q : When should we use Box over &?

pub fn strlen_dyn(s: Box<dyn AsRef<str>>) -> usize {...} pub fn strlen_dyn(s: &dyn AsRef<str>) -> usize {...}

A : Box 的優勢是 static。即使 caller 的 stack frame 消失後,你也可以繼續使用它。一般都會選 Box 來使用。

Q : Can I then also give the function a reference to the stack ?
A : 可以,如下 :

pub fn strlen_dyn(s: Box<dyn AsRef<str>>) -> usize { s.as_ref().as_ref().len() } +pub fn strlen_dyn2(s: &dyn AsRef<str>) -> usize +{ + s.as_ref().len() +} pub fn main() { let x: Box<dyn AsRef<str>> = Box::new(String::from("hello")); strlen_dyn(x); + let y: &dyn AsRef<str> = &"world"; + strlen_dyn2(y); }
目前程式碼
pub fn strlen_dyn(s: Box<dyn AsRef<str>>) -> usize { s.as_ref().as_ref().len() } pub fn strlen_dyn2(s: &dyn AsRef<str>) -> usize { s.as_ref().len() } pub fn main() { let x: Box<dyn AsRef<str>> = Box::new(String::from("hello")); strlen_dyn(x); let y: &dyn AsRef<str> = &"world"; strlen_dyn2(y); }

Can I Recover The Concrete Type?

0:46:47

你可以把它想像成基本上是型別擦除。從技術上講,你理論上可以通過 unsafe 的轉換等手段回溯,但一般來說,一旦將其轉換為 trait 物件,你就消除了它曾經是什麼型別的知識。

如果你做了 Box<dyn AsRef<str>> 以及 &dyn AsRef<str> 這兩件事,你僅保留使用這個 trait 的能力。你抹除了有關具體型別是什麼的所有其他知識。因此,在一個被包裝的 AsRef<str> 上,你唯一能做的事情就是對其呼叫 as_ref()

Dynamic Dispatch

0:47:56

思考編譯器如何產生 say_hei 函式的機械碼 :

... impl Hei for &str { fn hei(&self) { println!("hei {}", self); } } ... pub fn say_hei(s: &dyn Hei) { s.hei(); // call ??? // 當編譯器產生程式碼的時候,它不知道 s 的型別,s 就只是個指標, // 那這樣又怎麼呼叫 hei() 方法 ? // 答案是與 dynamic dispatch 跟 vtable 有關。 } // generic case pub fn say_hei_static<H: Hei>(s: H) { s.hei(); } // 每一個具體型別會得到 generic case 的 copy pub fn say_hei_static_str(s: &str) { s.hei(); // call Line 2 }

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
事實上,Box 的參考只有一個寬指標這一說法並不完全正確。它實際上攜帶了一些關於所指向的型別的額外資訊。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Dynamically Sized Types
Most types have a fixed size that is known at compile time and implement the trait Sized. A type with a size that is known only at run-time is called a dynamically sized type (DST) or, informally, an unsized type. Slices and trait objects are two examples of DSTs.

我們只能在執行時期才知道 s 的型別 :

pub fn main() { let random = xxx; // read from the user if random == 4 { say_hei(&"hello world"); } else { say_hei(&String::from("world")); } }

必須等到執行時期才知道 s 的型別究竟是 &str 還是 &String。

目前程式碼
pub trait Hei { fn hei(&self); } impl Hei for &str { fn hei(&self) { println!("hei {}", self); } } impl Hei for String { fn hei(&self) { println!("hei {}", self); } } pub fn say_hei(s: &dyn Hei) { s.hei(); } pub fn say_hei_static<H: Hei>(s: H) { s.hei(); } pub fn say_hei_static_str(s: &str) { s.hei(); } pub fn main() { let random = xxx; // read from the user if random == 4 { say_hei(&"hello world"); } else { say_hei(&String::from("world")); } }

Vtables

0:53:08

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Dynamically Sized Types
Pointer types to DSTs are sized but have twice the size of pointers to sized types

  • Pointers to slices also store the number of elements of the slice.
  • Pointers to trait objects also store a pointer to a vtable.

繼續討論 dynmaic dispatch 和 vtable 的 :

... impl Hei for &str { fn hei(&self) { println!("hei {}", self); } } impl Hei for String { fn hei(&self) { println!("hei {}", self); } } pub fn say_hei(s: &dyn Hei) { // &dyn Hei // stored in & // 1. a pointer to the actual, concrete, implementing type // 2. a pointer to a vtable for the referenced trait // // what is a vatable ? // vtable (virtual dispatch table) 是一個小型資料結構, // 它具有指向該型別 trait 的每個方法的指標。 // // dyn Hei, vtable: // struct HeiVtable { // hei: *mut Fn(*mut ()) // Line 2 // } // // 對於每個轉換為 trait 物件的具體型別,都會構建一個不同的 vtable。 // &str -> &dyn Hei // 1. pointer to the &str // Jon 0:58:50 才在 str 前面補 & // 2. HeiVtable { // hei: &<str as Hei>::hei // Line 2 // } // &String -> &dyn Hei // 1. pointer to the String // 2. HeiVtable { // hei: &<String as Hei>::hei // Line 10 // } s.hei(); // s.vtable.hei(s.pointer) } ...

Q : are the vtables themselves statically build at compile time or are they also allocated dynamically?
A : vtables 在編譯時構建,它們是 statically 構建的。指向 vtable 的指標也是 statically 已知的,因為該指標是由 trait 物件的原始構造決定的。

從技術上講你可以即時建造 vtable,因為 s.vtable.hei(s.pointer) 的呼叫中沒有任何內容要求它是 statically 已知的,所以你可以想像手動建立一個 vtable,我們稍後會討論到。

Q : but &str also contains the length of the string slice, no?
A : 是的。所以剛剛例子的第一個指標是指向 &str 而不是 str。

Q : can we debug print that vtable struct somehow?
A : Jon 不知道一種方法可以讓編譯器印出其 vtable 結構,但基本上 vtable 只是對於這是一個 trait 物件的相應 trait 的每個方法,每個成員都是相應的方法,並且該值是該方法的具體型別的實作的指標。

Q : Then why are trait functions that don't take self not object safe? Couldn't the compiler just generate a Fn(*mut Self) that doesn't use Self?
A : 等等將會談到。

Q : does it construct a new vtable every time we create an instance? like, 2 str heis construct two vtables?
A : 不,vtable 通常是為型別 statically 建構的,它不是dynamically 建構的。

Q : Are identical vtable detected?
A : 編譯器並不會被去重。實際上,編譯器保證 vtables 不是重複的。如果你為兩種不同的型別實作一個 trait,即使它們包含相同的程式碼,它們在原始碼和產生的二進位檔中仍然是不同的位置,因此將具有不同的地址。

Q : So Box<dyn ...> is a thin pointer that points to a wide pointer that points to the object?
A : 並不是,請看以下說明 :

// Box<dyn Hei> // Box 本身是一個寬指標,會額外儲存指向型別的資訊。 // Box 的成員若是以下型別 : // *mut dyn Hei // 編譯器看到 trait 物件,就會給兩個指標大小的空間。 // 所以不是用間接的方式去存取 trait 物件,即瘦指標指向一個寬指標。

Limitation: Multiple Traits

1:02:13

trait 物件的限制是,不能用多個指標指向不同的 vtable :

// 原本的函式簽章語法有誤 // pub fn baz(s: &dyn Hei + AsRef<str>) -> usize // 聊天室提供的語法修正 pub fn baz(s: &(dyn Hei + AsRef<str>)) -> usize { // 如果要可以呼叫不同 trait 的方法, // 必須要讓指標指向不同的 vtable, // 這樣 s 就變成要有 "兩個" 指標指向不同的 vtable。 // 技術上可行,但你的指標會隨著 trait 物件數量變動, // 這可能不會是你想要的。 // 編譯器目前只讓指標指向 "一個" vtable, // 不知道未來會不會支援多個。 s.hei(); let s = s.as_ref(); s.len() }
目前程式碼
pub trait Hei { fn hei(&self); } impl Hei for &str { fn hei(&self) { println!("hei {}", self); } } impl Hei for String { fn hei(&self) { println!("hei {}", self); } } pub fn say_hei(s: &dyn Hei) { s.hei(); } pub fn say_hei_static<H: Hei>(s: H) { s.hei(); } pub fn say_hei_static_str(s: &str) { s.hei(); } pub fn baz(s: &(dyn Hei + AsRef<str>)) -> usize { s.hei(); let s = s.as_ref(); s.len() } pub fn main() { let random = 4; // read from the user if random == 4 { say_hei(&"hello world"); } else { say_hei(&String::from("world")); } }

編譯器提示錯誤 :

$ cargo check
...
error[E0225]: only auto traits can be used as additional traits in a trait object
  --> src\lib.rs:37:27
   |
37 | pub fn baz(s: &(dyn Hei + AsRef<str>)) -> usize
   |                     ---   ^^^^^^^^^^ additional non-auto trait
   |                     |
   |                     first non-auto trait
   |
   = help: consider creating a new trait with all of these as supertraits and using that trait here instead: `trait NewTrait: Hei + AsRef<str> {}`
   = note: auto-traits like `Send` and `Sync` are traits that have special properties; for more information on them, visit <https://doc.rust-lang.org/reference/special-types-and-traits.html#auto-traits>
  • 編譯器的錯誤訊息的 help 有提供替代方法 :
    ​​​​pub trait HeiAsRef: Hei + AsRef<str> {} ​​​​// 現在指標只會指向 "一個" vtable ​​​​pub fn baz(s: &dyn HeiAsRef) -> usize ​​​​{ ​​​​ s.hei(); ​​​​ let s = s.as_ref(); ​​​​ s.len() ​​​​}
  • 編譯器的錯誤訊息的 note :
    • Send 沒有任何方法,因此 vtable 為空 :
      ​​​​​​​​// 我們不需要額外的空間儲存 vtable,因此以下寫法是 ok 的。 ​​​​​​​​pub fn baz(s: &(dyn HeiAsRef + Send)) -> usize ​​​​​​​​{ ​​​​​​​​ s.hei(); ​​​​​​​​ let s = s.as_ref(); ​​​​​​​​ s.len() ​​​​​​​​}
    • 不能自己定義一個沒有方法的 trait 並且像上面程式碼額外加一個 Send trait 來做使用 :
      ​​​​​​​​pub fn baz(s: &(dyn HeiAsRef + Foox)) -> usize ​​​​​​​​{ ​​​​​​​​ s.hei(); ​​​​​​​​ let s = s.as_ref(); ​​​​​​​​ s.len() ​​​​​​​​} ​​​​​​​​pub trait Foox {}
目前程式碼
pub trait Hei { fn hei(&self); } impl Hei for &str { fn hei(&self) { println!("hei {}", self); } } impl Hei for String { fn hei(&self) { println!("hei {}", self); } } pub fn say_hei(s: &dyn Hei) { s.hei(); } pub fn say_hei_static<H: Hei>(s: H) { s.hei(); } pub fn say_hei_static_str(s: &str) { s.hei(); } pub trait HeiAsRef: Hei + AsRef<str> {} pub fn baz(s: &dyn HeiAsRef) -> usize { s.hei(); let s = s.as_ref(); s.len() } pub fn main() { let random = 4; // read from the user if random == 4 { say_hei(&"hello world"); } else { say_hei(&String::from("world")); } }

Limitation: Associated Types

1:08:32

trait 物件還有另一個限制是 Associated Types :

pub trait Hei { + type Name; fn hei(&self); } impl Hei for &str { + type Name = (); fn hei(&self) { println!("hei {}", self); } } impl Hei for String { + type Name = (); fn hei(&self) { println!("hei {}", self); } } ...
目前程式碼
pub trait Hei { type Name; fn hei(&self); } impl Hei for &str { type Name = (); fn hei(&self) { println!("hei {}", self); } } impl Hei for String { type Name = (); fn hei(&self) { println!("hei {}", self); } } pub fn say_hei(s: &dyn Hei) { s.hei(); } pub fn say_hei_static<H: Hei>(s: H) { s.hei(); } pub fn say_hei_static_str(s: &str) { s.hei(); } pub trait HeiAsRef: Hei + AsRef<str> {} pub fn baz(s: &dyn HeiAsRef) -> usize { s.hei(); let s = s.as_ref(); s.len() } pub fn main() { let random = 4; // read from the user if random == 4 { say_hei(&"hello world"); } else { say_hei(&String::from("world")); } }

編譯後會出現以下錯誤 :

$ cargo check 
...
error[E0191]: the value of the associated type `Name` (from trait `Hei`) must be specified
  --> src\lib.rs:25:24
   |
3  |     type Name;
   |     --------- `Name` defined here
...
25 | pub fn say_hei(s: &dyn Hei) 
   |                        ^^^ help: specify the associated type: `Hei<Name = Type>`
...

編譯器的錯誤訊息是說,當我們傳入一個 &dyn Hei 時,我們實際上需要指定 associated types。我們不能僅僅說我們傳入任何 Hei,而不考慮其 associated types 是什麼。原因在於 type Name 的資訊無法在 vtable 中獲得,因為 type Name 是一個型別,type Name 在二進位檔中沒有地址,沒有任何我們可以放入 vtable 中的東西。因此,type Name 不能直接用於 trait 物件。

你需要這樣做 :

... -pub fn say_hei(s: &dyn Hei) +pub fn say_hei(s: &dyn Hei<Name = ()>) { ... } ... + // 這裡應該不用改 ? Jon 有改這裡。 -pub trait HeiAsRef: Hei + AsRef<str> {} +pub trait HeiAsRef: Hei<Name = ()> + AsRef<str> {} -pub fn baz(s: &dyn HeiAsRef) -> usize +pub fn baz(s: &dyn HeiAsRef<Name = ()>) -> usize { ... } ...
目前程式碼
pub trait Hei { type Name; fn hei(&self); } impl Hei for &str { type Name = (); fn hei(&self) { println!("hei {}", self); } } impl Hei for String { type Name = (); fn hei(&self) { println!("hei {}", self); } } pub fn say_hei(s: &dyn Hei<Name = ()>) { s.hei(); } pub fn say_hei_static<H: Hei>(s: H) { s.hei(); } pub fn say_hei_static_str(s: &str) { s.hei(); } pub trait HeiAsRef: Hei + AsRef<str> {} pub fn baz(s: &dyn HeiAsRef<Name = ()>) -> usize { s.hei(); let s = s.as_ref(); s.len() } pub fn main() { let random = 4; // read from the user if random == 4 { say_hei(&"hello world"); } else { say_hei(&String::from("world")); } }

Limitation: Static Trait Methods

1:10:30

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
程式碼沿用 Limitation: Multiple Traits 章節最後的目前程式碼。

新增 weird() 函式

pub trait Hei { fn hei(&self); + fn weird() {} }
目前程式碼
pub trait Hei { fn hei(&self); fn weird() {} } impl Hei for &str { fn hei(&self) { println!("hei {}", self); } } impl Hei for String { fn hei(&self) { println!("hei {}", self); } } pub fn say_hei(s: &dyn Hei) { s.hei(); } pub fn say_hei_static<H: Hei>(s: H) { s.hei(); } pub fn say_hei_static_str(s: &str) { s.hei(); } /* 暫時註解掉,我們想要觀察別的編譯錯誤 pub trait HeiAsRef: Hei + AsRef<str> {} pub fn baz(s: &dyn HeiAsRef) -> usize { s.hei(); let s = s.as_ref(); s.len() } */ pub fn main() { let random = 4; // read from the user if random == 4 { say_hei(&"hello world"); } else { say_hei(&String::from("world")); } }

編譯得到以下錯誤 :

$ cargo check ... error[E0038]: the trait `Hei` cannot be made into an object --> src\lib.rs:24:20 | 24 | pub fn say_hei(s: &dyn Hei) | ^^^^^^^ `Hei` cannot be made into an object | note: for a trait to be "object safe" it needs to allow building a vtable to allow the call to be resolvable dynamically; for more information visit <https://doc.rust-lang.org/reference/items/traits.html#object-safety> --> src\lib.rs:5:8 | 1 | pub trait Hei | --- this trait cannot be made into an object... ... 5 | fn weird() {} | ^^^^^ ...because associated function `weird` has no `self` parameter help: consider turning `weird` into a method by giving it a `&self` argument | 5 | fn weird(&self) {} | +++++ help: alternatively, consider constraining `weird` so it does not apply to trait objects | 5 | fn weird() where Self: Sized {} | +++++++++++++++++

雖然編譯器有提示我們該怎麼解決,但它並沒有說明為什麼一定要有 &self 參數。修改 say_hei 函式來說明原因 :

pub fn say_hei(s: &dyn Hei) { - s.hei(); + s.weird(); }

因为 weird 函式沒有接收 self,它沒有接收指向任何東西的指標。我們不能隨便建構一個指向 weird 函式的指標,因為這意味著我們需要有一個有效的 instance 才能有指向weird 函式的指標。上面的程式碼就有點像 Python 的 classmethod :

pub fn say_hei() { (dyn Hei)::weird(); // 這裡沒有任何東西指定我們想要哪個實際的 weird 實作。 }

在這個特定情況下,因為 pub trait Hei 提供了一個預設的 weird() 函式,你可能會認為這不是問題。

如果 &str 裡面也實作了 weird() 函式 :

impl Hei for &str { fn hei(&self) { println!("hei {}", self); } + fn weird() {} }

因為現在沒有 &self 的資訊,所以編譯器無法判斷是要用到預設的 weird() 函式,還是 &str 裡面的 weird() 函式。

但如果我真的想要這樣做怎麼辦? 我不在乎透過 trait 物件呼叫 weird() 函式,我只關心 hei() 函式。所以我希望 Hei trait 是 object safe。如果我有一個 trait 物件 Hei,我不介意無法呼叫它的 weird() 函式,可以這麼做 :

pub trait Hei { fn hei(&self); - fn weird() {} + fn weird() where Self: Sized {} }

這樣的作法基本上是一種選擇退出 vtable 的方式,這意味著 weird() 函式不應該被放置在 vtable 中,最重要的是不應該透過 trait 物件來呼叫。

目前程式碼
pub trait Hei { fn hei(&self); fn weird() {} where Self: Sized {} } impl Hei for &str { fn hei(&self) { println!("hei {}", self); } } impl Hei for String { fn hei(&self) { println!("hei {}", self); } } pub fn say_hei(s: &dyn Hei) { s.hei(); } pub fn say_hei_static<H: Hei>(s: H) { s.hei(); } pub fn say_hei_static_str(s: &str) { s.hei(); } /* 暫時註解掉,我們想要觀察別的編譯錯誤 pub trait HeiAsRef: Hei + AsRef<str> {} pub fn baz(s: &dyn HeiAsRef) -> usize { s.hei(); let s = s.as_ref(); s.len() } */ pub fn main() { let random = 4; // read from the user if random == 4 { say_hei(&"hello world"); } else { say_hei(&String::from("world")); } }

即便你將 &self 函式傳入 weird() 函式,weird() 函式仍可以不被放在 vtable 中 :

pub trait Hei { fn hei(&self); - fn weird() where Self: Sized {} + fn weird(&self) where Self: Sized {} } impl Hei for &str { fn hei(&self) { println!("hei {}", self); } - fn weird() {} + // 這裡是為了避免重複定義同一函式才移除的 : + // error[E0186]: method `weird` has a `&self` declaration in the trait, but not in the impl }

接著嘗試在 say_hei() 函式裡面呼叫 weird() 函式 :

pub fn say_hei(s: &dyn Hei) { s.hei(); + s.weird(); }
目前程式碼
pub trait Hei { fn hei(&self); fn weird(&self) where Self: Sized {} } impl Hei for &str { fn hei(&self) { println!("hei {}", self); } } impl Hei for String { fn hei(&self) { println!("hei {}", self); } } pub fn say_hei(s: &dyn Hei) { s.hei(); s.weird(); } pub fn say_hei_static<H: Hei>(s: H) { s.hei(); } pub fn say_hei_static_str(s: &str) { s.hei(); } /* 暫時註解掉,我們想要觀察別的編譯錯誤 pub trait HeiAsRef: Hei + AsRef<str> {} pub fn baz(s: &dyn HeiAsRef) -> usize { s.hei(); let s = s.as_ref(); s.len() } */ pub fn main() { let random = 4; // read from the user if random == 4 { say_hei(&"hello world"); } else { say_hei(&String::from("world")); } }

編譯器如預期的報錯,因為我們本來就沒想要透過 trait 物件來呼叫 weird() 函式 :

$ cargo check ... error: the `weird` method cannot be invoked on a trait object --> src\lib.rs:27:7 | 5 | fn weird(&self) where Self: Sized {} | ----- this has a `Sized` requirement ... 27 | s.weird(); | ^^^^^ ...

Disallowing Trait Objects

1:15:48

你也可以讓整個 trait 不能作為 trait 物件使用 :

pub trait Hei +where + Self: Sized; { fn hei(&self); }

選擇禁止 trait 物件的情況很少見。有時人們這樣做是出於向後相容性的原因。比如,如果你知道以後可能會添加非 object safe 的方法,你可能會預先添加這個,但這種情況相當罕見。

Q : does the associated type problem also occur with static dispatch?
A : 不,它不會。原因在於 static dispatch,因為它會被 monomorphize,所以你實際上可以知道任何給定函式實作的具體型別。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
1:17:06
Q : s::weird() could be possible

pub trait Hei { fn hei(&self); + fn weird() {} } impl Hei for &str { fn hei(&self) { println!("hei {}", self); } + fn weird() {} } ... pub fn say_hei(s: &dyn Hei) { - s.hei(); + s::weird(); } ...

當你在 Line 21 呼叫 s 的 weird() 方法,你可以想像 weird() 方法被包含在 trait 物件的 vtable 中,但 s 知道不帶 &self 參數呼叫 weird() 方法。這裡的挑戰在於這有點奇怪,如果呼叫 s 需要型別的 instance,那麼為什麼 weird() 不直接接收 &self 參數呢?

在傳統的例子中,weird() 方法實際上被稱為 new() 方法 :

pub trait Hei { fn hei(&self); - fn weird() {} + fn new() {} } impl Hei for &str { fn hei(&self) { println!("hei {}", self); } - fn weird() {} + fn new() {} } ... pub fn say_hei(s: &dyn Hei) { - s.hei(); + s::weird(); } ...

如果你已經有了該型別的 instance,則不需要先呼叫該方法,它應該直接接受 &self 參數。

Jon 同意這樣的事情也許是可能的,但在實踐中,它通常不是很有用。


Q : Doesn't where Self: Sized also disallow implementing the trait for concrete DSTs?
A : 還是可以實作,如下 :

pub trait Hei where Self: Sized { fn hei(&self); } // 因為 Box<dyn AsRef<str>> 是 Sized 而不是 dynamic sized, // 所以這個實作編譯不會有問題。 impl Hei for Box<dyn AsRef<str>> { fn hei(&self) { println!("hei {}", self.as_ref().as_ref()); } }

Q : @jonhoo I meant types such as str, or [u8]
A : 你不能這麼做,因為 Self 必須是 Sized。


Q : The associated type restriction feels weird because the point of associated types is that only one exists for any given concrete type. So shouldn't the vtable know what they associated type should be?
A : 問題是型別被擦除了,只剩下的就是 vtable,所以你無法從 vtable 中看出具體型別曾經是什麼。
有一個例外情況,那就是 Any trait。Any trait 有一個方法,回傳它曾經是的具體型別的 descriptor。如果你沒有理解這一點,沒關係,可以忽略它。但基本上,有一些方法可以繞過這個問題。但基本上,trait 物件是型別擦除的,你不能保留有關它曾經的型別的訊息。

Limitation: Generic Methods

1:20:58

FromIterator trait 是由 Vec 結構實作的。先觀察 colloect() 函式,由於 collect() 函式要求收集的東西要實作 FromIterator trait,而 FromIterator trait 又是 Vec 結構實作的,所以在呼叫 collect 的時候,會把元素放到 Vec 中 :

fn collect<B>(self) -> B where B: FromIterator<Self::Item>, Self: Sized,

接著觀察 FromIterator trait 簽章 :

// A 型別是迭代器 item 的型別 pub trait FromIterator<A>: Sized { // 1. T 是迭代器的型別。例如 : 你可以從 "任何型別的迭代器" 建構一個 Vec<A> // 2. 這個函式造成 FromIterator trait 是 not object safe。 fn from_iter<T>(iter: T) -> Self where T: IntoIterator<Item = A>; }

因為 extend() 要求接收 &self 參數,改用 extend() 的程式來說明。以下程式使用了 trait 物件的泛型函式讓 trait 不是 object safe :

use std::iter::Extend; pub fn add_true(v: &mut dyn Extend<bool>) { v.extend(std::iter::once(true)); }

看一下 Extend trait 的簽章 :

pub trait Extend<A> { // 1. extend 會接收 &self 參數,所以我們看的到 vtable。 // 給定一個 instance,我們就知道如何呼叫這個方法。 // 2. 該方法是泛型的 ! fn extend<T>(&mut self, iter: T) where T: IntoIterator<Item = A>; ... }

接著看 extend() 泛型函式:

struct MyVec<T>(Vec<T>); impl<T> Extend<T> for MyVec<T> { // 1. 我們從 monomorphization 討論中知道, // 實際上在編譯時,我們最終不會得到單一 extend。 // 2. 對於每一種迭代器和 item 型別的組合都有一個 extend 方法。 // 所以 vtable 中要放入什麼呢? fn extend<I>(&mut self, iter: I) where I: IntoIterator<Item = T>, { // ... } // 會有多個 extend 的 copy /* fn extend_hashmap_intoiter_bool( &mut self, iter: std::collection::hash_map::IntoIterator<bool>) where I: IntoIterator<Item = T>, { // ... } */ }

繼續觀察 add_true 函式 :

pub fn add_true(v: &mut dyn Extend<bool>) { v.extend(std::iter::once(true)); }

雖然我們傳入的 &mut dyn Extend<bool> 讓 T 為已知,但這個 trait 物件並沒有攜帶 extend() 函式要用到的 I 資訊,我們甚至沒有辦法讓傳入的 trait 物件攜帶這 I 的資訊。當編譯器嘗試產生 Line 3 的程式碼時,並沒有指向適當的 extend() 實作的指標。

因此,編譯器無法為 dyn Extend 產生 vtable,因為它會有無限多個 entry,每個 entry 都對應一個可能的 extend 實作。因此,答案只是 dyn Extend 無法存在,你無法為 extend 創建一個 trait 物件。

Q : I can sort of see why we don't want this, but could rustc add a monomorphized version of Extend::extend for each T it's called with, to each type that implements Extend?
A : 這很誘人,但並不總是可行。標準函式庫中有許多對 extend 的使用,但在依賴該標準函式庫的 crate 中,它們可能會使用更多不同的型別來呼叫 extend。在編譯 crate 時,底層的 crate 中 dyn Extend 的 vtable 與更高層 crate 中 dyn Extend 的 vtable 是不同的,因為你可能想要使用更多可能的迭代器實作。因此,你最終將會得到許多不同的 dyn Extend 的 vtable,這就像是一種組合爆炸的問題。這也意味著你不能將來自標準函式庫的 dyn Extend 傳遞給更高層 crate 中接受 dyn Extend 的物件,因為它們的 vtable 型別是不同的

Limitation: No Non-Receiver Self

1:30:53

要讓 trait 物件是 object safe 需要幾個條件 :

  1. 沒有泛型方法
  2. 所有方法都需要有一個包含 &self 的 receiver
  3. 方法不能回傳 Self。
    ​​​​// fn clone(&self) -> Self ​​​​pub fn clone(v: &dyn Clone) ​​​​{ ​​​​ let x = v.clone(); ​​​​ // v 回傳 dyn Clone (因為 clone 回傳 Self) ​​​​ // dyn Clone 不是 Sized,x 無法知道大小。 ​​​​}

Partial Object Safety

1:33:00

就像我們與 Hei 討論過的一樣,你可能有一些方法是 object safe,它們本身仍然很有用,但你可能還有一堆其他方法會使得該 trait 變成 non object safe。然而,你可能希望將這些方法 include 在那些不會通過 trait 物件的型別中,只因為這樣很方便。

看到 Trait std::iter::Iterator,就可以一堆輕鬆找到 non object safe 的方法 :

pub trait Iterator { type Item; // object safe fn next(&mut self) -> Option<Self::Item>; // non object safe,因為泛型方法 fn chain<U>(self, other: U) -> Chain<Self, <U as IntoIterator>::IntoIter> where Self: Sized, U: IntoIterator<Item = Self::Item> { ... } // non object safe,因為回傳 Self fn enumerate(self) -> Enumerate<Self> where Self: Sized { ... } // non object safe,因為回傳 Self fn by_ref(&mut self) -> &mut Self where Self: Sized { ... } // non object safe,因為泛型方法 fn collect<B>(self) -> B where B: FromIterator<Self::Item>, Self: Sized { ... } ... }

既然 Iterator 這麼多 non object safe 的方法,那 Iterator 本身為什麼是 object safe ?

pub fn it(v: &mut dyn Iterator<Item = bool>) { // 不能呼叫 collect // let x: Vec<bool> = v.collect(); // 可以呼叫 next let _ = v.next(); }

答案是 non object safe 方法都有 Self: Sized 的條件,讓這些方法不要放進 vtable,也就是我們前面在處理 weird() 方法用到的技巧 :

pub trait Iterator { type Item; // non object safe,因為沒有接收 &self // 註解 : count() 不允許在參考背後進行操作,因為傳入參數不是 &self。 // count() 方法會耗用 self,這意味著它接受 self, // 而 self 是一個 dyn Iterator,它是非固定大小的,而函式參數必須是固定大小的, // 因此 count() 方法不能透過 trait 物件呼叫。 fn count(self) -> usize where Self: Sized { ... } // non object safe,因為泛型方法 + 回傳 Self + 沒有接收 &self fn chain<U>(self, other: U) -> Chain<Self, <U as IntoIterator>::IntoIter> where Self: Sized, U: IntoIterator<Item = Self::Item> { ... }

如果擁有一個 trait 物件,則無法呼叫 non object safe 方法。但如果沒有一個 trait 物件,那麼就可以呼叫 non object safe 方法。這種方式可以讓一個 trait 擁有一些很好用的方法,但在不將整個 trait 變成 non object safe 的情況下,non object safe 方法可能不會是 object safe。

Q : Can the receiver be anything that includes self or does it have to be &self or &mut self?
A : Object Safety 提到的我們幾乎都有提到 :

  • All supertraits must also be object safe.
    (因為我們從所有 supertraits 的聯集構建 vtable)
  • Sized must not be a supertrait. In other words, it must not require Self: Sized.
    (如果要求 Self: Sized,就是 non object safe 方法)
  • It must not have any associated constants.
    (因為 &dyn xxx<XXX> 沒地方放常數值。也許你可以常數值放到 vtable,但這樣 vtable 會變很大,而且這樣做會讓 vtable 中的值不再都是函式指標,它們可能是任意 Sized 的東西。)
  • All associated functions must either be dispatchable from a trait object or be explicitly non-dispatchable :
    • Dispatchable functions must:
      • Not have any type parameters (although lifetime parameters are allowed).
      • Be a method that does not use Self except in the type of the receiver.
      • Have a receiver with one of the following types:

Q : Should library writers always consider adding where Self: Sized to non-object-safe methods just in case someone downstream wants to use it as a trait object?
A : 是的。這有點取決於你的 trait 是否可用。所以如果你的 trait 作為一個 trait 物件是不可用的,比如說 clone,那麼你當然可以為 clone 方法添加 where Self: Sized 條件,這樣人們就可以創建一個 trait 物件的 clone,但是他們就無法呼叫在 main 函式呼叫該 trait了,所以這可能不值得。通常來說,如果你有一個 trait ,在這個 trait 中有用,即使你只能呼叫 object safe 方法,那麼放棄其他方法可能是有道理的,這樣整個 trait 就是 object safe 的。

Q : did iterator last have the sized restriction? why, given next doesn't?

fn last(self) -> Option<Self::Item> where Self: Sized { ... }

A : last() 不允許在參考背後進行操作,因為傳入參數不是 &self。last() 方法會耗用 self,這意味著它接受 self,而 self 是一個 dyn Iterator,它是非固定大小的,而函式參數必須是固定大小的,因此 last() 方法不能透過 trait 物件呼叫。

Dropping Trait Objects

1:39:54

  • 繼續揭密 trait 物件,drop trait 是 object safe :
    ​​​​pub fn drop(v: &mut dyb Drop) ​​​​{ ​​​​ // when v goes out of scope, Drop::drop is called ​​​​}
    你可能會認為這很奇怪。如果你對一個物件所能做的唯一事情就是卸除它,那麼它真的那麼有趣嗎?事實證明,在一些情況下這是有用的。crossbeam 就是這樣做的,例如,當你想要進行垃圾回收,但又不想立即卸除物件時,你想將它們放入類似於鏈結串列的東西中,然後定期去收集所有的垃圾。因為你想要在一種型別中儲存大量可能不同的東西,所以你需要 trait 物件。因此它只是儲存 &mut dyn Drop,或者技術上是以 Box 方式在卸除,但效果相同。
  • 接著討論當 trait 物件離開 scrope 時,會發生什麼事 :
    ​​​​pub fn say_hei(s: Box<dyn AsRef<str>>) ​​​​{ ​​​​ // what happens when s goes out of scope ? ​​​​}
    假設現在我們獲得一個 Box,所以在 heap 上有一個記憶體配置,我們可能在 say_hei() 函式使用它,可能呼叫 s.as_ref()。當這個函式返回時,我們會卸除 s,但是當我們卸除一個 Box 時,我們必須釋放記憶體,但 dyn AsRef<str> 是一個 trait 物件,所以它唯一的方法就是 as_ref()。對於這個問題的答案是每個 vtable 都包含 Drop。你可以想像 trait 物件 implicitly 有一個 Drop :
    ​​​​pub fn say_hei(s: Box<dyn AsRef<str> + Drop>) ​​​​{ ​​​​ // what happens when s goes out of scope ? ​​​​}
    但在實踐中,對於任何 trait 物件,vtable 都包含指向具體型別的 Drop 函式的指標,因為這是必要的。trait 物件技術上還包含一些額外的資訊,它包括具體型別的大小和對齊,這些資訊在裡面是因為對於像是 Box 這樣需要釋放記憶體的情況,這些資訊是必要的,以便傳遞給記憶體配置器以進行釋放記憶體。因此,每個 trait 物件的 vtable 都包括該 trait 的方法、Drop、大小和對齊。通常,你不必考慮這些,但在我們討論這個話題時,了解這一點是值得的。

Dynamically Sized Types

1:43:03

以下型別皆不是 Sized :

// dyn trait -> * -> (*mut data, *mut vtable) // [u8] -> * -> (*mut data, usize length) // str -> * -> (*mut data, usize length) // 當你嘗試使用 [u8] 作為參數傳遞時,編譯器會提示錯誤, // 因為這個寫法只告訴編譯器說,你想要傳入 u8 的任意長度 list。 fn foo(s: [u8]) {} // 回傳 [u8] 也會得到相同的錯誤 fn bar() -> [u8] { [][..] }

要讓型別從 Sized 變成 Sized 的技巧都是用指標去指向它 :

fn foo(s: &[u8]) {} fn bar() -> Box<[u8] > { Box::new([]) as Box<[u8]> }

目前 Dynamically Sized Types 在編譯器中有點特殊,因為你需要知道指標是否是寬型的,因此,你自己處理 not Sized 的型別相當困難。就像如果你想自己實作 Box 並指向 not Sized 的型別,這是可能的,但是一旦涉及到轉換和允許 trait 物件等事情,這就變得相當煩人。但最近有一個 RFC 2580-ptr-meta 獲得了通過 :

  • Add generic APIs that allow manipulating the metadata of fat pointers:
    (讓你告訴編譯器這個 fat pointer 的第二部分的型別是什麼)
    ​​​​trait DynTrait<Dyn> = Pointee<Metadata=DynMetadata<Dyn>>; ​​​​// Pointee 是 u8 slice (沒有 &) 實作的 ​​​​// Metadata 是 usize : 表示東西的長度

    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →
    DynMetadata
    It is a pointer to a vtable (virtual call table) that represents all the necessary information to manipulate the concrete type stored inside a trait object. The vtable notably it contains:

    • type size
    • type alignment
    • a pointer to the type’s drop_in_place impl (may be a no-op for plain-old-data)
    • pointers to all the methods for the type’s implementation of the trait

再看看幾個 RFC 內提到的東西 :

  • A Thin trait alias. If this RFC is implemented before type aliases are, uses of Thin should be replaced with its definition.
    Thin 是任何實作 Pointee 的型別,其中 Metadata 為空,也就是指標中沒有關聯的 Metadata,因此它是 thin pointer 而不是 fat pointer。
  • 有用於檢視指標的 metadata 的方法,因此你可以實際查看 vtable 等內容。這個 RFC 實際上讓你可以內省這些資訊,並且動態地建構 vtable。因此,你可以在編譯時未知的情況下,在執行時期建構 vtable。

Manual Vtables in std

1:48:30

Struct std::task::Waker 有使用動態地建構 table 的方法。

先看到 Waker 的實作 :

pub struct Waker { waker: RawWaker, }

繼續追蹤 RawWaker :

pub struct RawWaker { /// A data pointer, which can be used to store arbitrary data as required /// by the executor. This could be e.g. a type-erased pointer to an `Arc` /// that is associated with the task. /// The value of this field gets passed to all functions that are part of /// the vtable as the first parameter. data: *const (), /// Virtual function pointer table that customizes the behavior of this waker. vtable: &'static RawWakerVTable, }

接著看到 RawWakerVTable 的 new() :

pub const fn new( clone: unsafe fn(_: *const ()) -> RawWaker, wake: unsafe fn(_: *const ()), wake_by_ref: unsafe fn(_: *const ()), drop: unsafe fn(_: *const ()) ) -> RawWakerVTable // 手動建構 vtable, // 這個 vtable 透過 "型別" 而不是 trait 提供了動態分派。 // 剩下細節自己看 !

Q : thought &[u8] had a start pointer and an end pointer, not a length
A : Jon 認為只是 length。

Q&A: Making Your Own DST

1:51:45

Q : can we make our own types (DSTs)
A : 可以,如下 :

struct Foo { f: bool, t: [u8], // not Sized // x: bool, // t 後面不能再有欄位了 ! } &Foo (*Foo, length t) // Sized

Box([u8]) vs. Vec(u8)

1:53:41

Q : Is Box<[u8]> the same thing as Vec<u8>

Box<[u8]> == Vec<u8> ?

A: 它們不一樣。Vec<[u8]> 有三個 words,分別是指向位於 heap 的 vector 的指標,vector 的長度以及 vector 的容量。當 vector 的長度大於容量時,vector 會長大,但 Box<[u8]> 不能這麼做,[u8] 無法變大。你可以將 Vec<[u8]> 轉成 Box<[u8]>,也可以將 Box<[u8]> 轉成 Vec<[u8]>

dyn Fn() vs. fn() vs. impl Fn()

1:55:18

Q : Can we talk about the difference between dyn Fn and function pointers (fn)?
A : 它們不一樣,說明如下 :

// dyn Fn != fn fn foo(f: &dyn Fn) {} // &dyn Fn 是 trait 物件, // 它是寬指標,一個指向 vtable,另一個指向資料 fn bar(f: fn()) {} // f 必須為函式,不可以是 closure, // 因為 fn() 是函式指標,而指標就只是個位址而已。 // 最主要的區別還是 foo 的 f 可以是 closure fn main() { let x = "Hello"; foo(&|| { let _ = &x; }); // 它是一個函式指標以及 closure 從其環境中捕獲的資料。 // 當你呼叫 closure 時,你也需要提供 x 的位址, // 因為 closure 的 body 需要它,它就是傳遞給寬指標資料部分的內容。 // bar(&|| { // let _ = &x; // }); // bar 只要求是函式指標, // 所以當你嘗試用 closure 傳入 bar 時, // 它會沒有地方可以放資料。 }

Q : when would you use dyn Fn over impl Fn?

fn baz(f: impl Fn()) {} fn main() { baz(&|| { let _ = &x; }); }

A : baz 可以傳入 closure,因為 impl Fn() 在某種程度上是對任何是 fn 的泛型函式的語法糖。因此,我們實際上為每種傳入的 closure 型別都得到了 baz 的具體 copy。因此,你也可以輕鬆地傳遞資料,因為它被 monomorphizes 到每個獨立的 closure。

現在的問題變成,dyn Fnimpl Fn 使用時機是什麼 ? 答案是,impl Fn 更通用。因為你不需要在指標後面進行間接操作,但你最終會為每個傳入的 closure 型別產生一個 baz 的 copy,這可能會變得相當多。另一個原因是,有時你想要取一個 trait 物件而不是使其成為泛型,因為否則你必須將泛型向上傳遞。

看到 struct 的例子 :

pub struct Wrapper<F: Fn()> { f: F, } // 任何使用這個結構的使用者如果想要, // 也必須自己對 f 泛型化,或者指定 f 的型別。 pub struct Wrapper2 { f: Box<dyn Fn()>, } // Wrapper2 不再是泛型的, // 因此 caller 不需要考慮和傳遞該泛型參數。 // 所以有時它會清理你的介面並使其更好。

另外還有 trait 的例子 :

  • not object safe
    ​​​​trait x ​​​​{ ​​​​ fn foo(&self, f: impl Fn()); // 等價於 fn foo<F: Fn>(&self, f: F) ​​​​ // not object safe,因為 impl Fn() 是泛型。 ​​​​} ​​​​fn quox(x: &dyn x) {}
  • object safe
    ​​​​trait x ​​​​{ ​​​​ fn foo(&self, f: &dyn Fn()); ​​​​ // object safe,因為我們只有一個 foo ​​​​} ​​​​fn quox(x: &dyn x) {}

No Coherence This Stream

2:02:00

Coherence 會在另一個直播談到 (沒找到)。

Runtime Trait Detection

2:03:00

Q : could runtime trait detection be implemented in the future using a type's vtable?
(Jon 用他的想法理解聊天室的問題 : "你可以接受一個 trait 物件,或者你可以接受一個寬指標並通過查看其 vtable 來確定它實作了哪些 trait")
A : Jon 認為不行。部分原因是因為每個 trait 物件都會產生不同的 vtable,所以並不是對於 &str 或 String 有一個 vtable。對於 String,有許多 vtable,對於每個 dyn Trait,都有一個 String 的 vtable。所以不僅僅是一個 vtable,你可以通過查看它來找出它實作了哪些 trait。例如你有以下函式 :

fn find_traits(s: &dyn Trait) // vtable 中的唯一東西就是你在此處命名的 trait 上的任何方法, //

vtable 中的唯一東西就是你在此處命名的 Trait 上的任何方法。你將不會得到提供的型別的所有可能方法的 vtable,所以 Jon 不認為你可以透過這種方式動態檢測某些東西實作了哪些 trait。

Double-Dereferencing dyn Fn()

2:04:41

Q : @jonhoo Does calling a dyn Fn involve a double dereference, once for the vtable pointer and once for the actual function pointer within? Or does that get optimized away?
A : Jon 認為會解兩次不同指標的參考,而不是同一個指標被解參考兩次 :

fn call(f: &dyn Fn()) // fn call((t, vtable): (&T, &TsFnVtable) { f() // ((*vtable).call))(t) // 解參考 vtable 去找到 call 函式的位址 // 找到之後在呼叫 call 函式 }

Unsafe Vtable Comparions

2:06:55

你可以做到類似以下醜醜的比較 vtable 的功能,但建議不要這麼做 :

fn x(s: &mut dyn AsRef<str>) { // 編譯器會確保右式的型別只有一個 vtable。 if s.vtable == <&String as dyn AsRef<str>>.table { (s.data as &mut String).push() } }

一般而言,如果你使用實際的泛型參數或 impl trait,那麼在這些情況下,你將獲得更好的最佳化,因為編譯器在編譯時完全了解所有相關型別,並且可以根據實際的具體實作進行共同最佳化。而一旦通過 dynamic dispatch 進行間接呼叫,編譯器將失去一些本應擁有並能夠根據其進行最佳化的資訊。

Slice of Trait Objects

2:09:06

Q : can you do a quick example of the slice/vec of dyns?
A : 如下 :

// [] 裡面的元素要是指標,這樣才會是 Sized fn say_hei(v: &[&dyn AsRef<str>]) { for s in v { s.as_ref(); } }

Codegen Units and Vtables

2:10:05

Q : Different compilation units can lead to different vtables, I think there's an issue on that somewhere, and even a clippy lint
A : 大致是對的。在編譯 Rust 程式碼時,Rust 可能使用多個獨立的執行緒來編譯同一個 crate,以編譯該 crate 的不同子集,從而加速編譯。但是因為你希望這些獨立的單元完全並行地工作,而不需要太多的同步,它們可能都會遇到,比如,&dyn AsRef<str>,它們可能都會產生自己的 vtable,因為它們不想協調產生 vtable,因此即使是相同的型別的相同 trait,也可能有多個 vtable。這就是為什麼 invaraint 可能不成立的一個例子。

The Any Trait

2:10:55

Trait std::any::Any

Any trait 的簽章 :

pub trait Any: 'static { // 回傳 type identifier (這是唯一的,由編譯器保證) fn type_id(&self) -> TypeId; }

如果有一個對 Any 的 trait 物件,你可以對其使用 TypeId,因為它被添加到 vtable 中以獲得該值的唯一的 type identifier,然後可以使用該 type identifier 從 dyn Any downcast 成具體型別,因為我們知道該 type identifier 是什麼,也就是你可以從 dyn Trait(只要該 Trait 包含 Any trait)轉換為對實際底層型別的參考。

待整理

  1. 1:17:06
  2. 自己看 2580-ptr-meta
  3. 自己看 Module std::any