Zach的博客

Rust-Object-Safe

Rust中同时支持动态分派和静态分派。

静态分派

所谓静态分派,是指编译器就知道调用哪一个函数来实现多态。举个例子来说,我们有一个Foo Trait,并为u8和String实现了这个Trait:

1
2
3
4
5
6
7
8
9
10
11
trait Foo {
method(&self) -> Stirng;
}

impl Foo for u8 {
fn method(&self) -> String { format!("u8: {}", *self) }
}

impl Foo for String {
fn method(&self) -> String { format!("string: {}", *self) }
}

如果我们定义如下的泛型函数

1
2
3
fn do_something<T: Foo>(x: T) {
println!("{}", x.method());
}

并这样调用这一个泛型函数

1
2
3
4
5
6
7
fn main() {
let x = 5u8;
let y = "Hello".to_string();

do_something(x);
do_something(y);
}

由于在编译阶段编译器已经知道了x和y的类型,那么编译器会在调用泛型函数的时候展开函数,即编译器会生成类似下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fn do_something_u8(x: u8) {
x.method();
}

fn do_something_string(x: String) {
x.method();
}

fn main() {
let x = 5u8;
let y = "Hello".to_string();

do_something_u8(x);
do_something_string(y);
}

这样一来,泛型函数的调用就变成了特定类型展开结果的内联调用,这样的优点是显而易见的:内联函数易于优化,而且快!当然,内联函数过多,代码也会变成臃肿,同时”优化“也并不见得是真的优化。

动态分派

由于静态分派有瑕疵,也就有了动态分派。动态分派是通过Trait Object实现的,Trait Object指的是类型直到运行时才能确定的对象,如&Foo,Box\等,其实也就是指向Trait的指针了。

一个Trait Object其实是一个胖指针,它的内存结构为:

1
2
3
4
5

pub struct TraitObject {
pub data: *mut (),
pub vtable: *mut (),
}

可以看到Trait Object是通过虚函数表来实现动态分派的,但是Rust的虚函数表和C++的虚函数表不同的是,在C++中,每一个对象实例中都存在一个vtable指针,而在Rust中,对象实例是没有vtable指针的,vtable阵阵存在于Trait Object中。

一个vtable实际上就是包裹了函数指针的结构,还是以Foo这个Trait为例子,一个可能的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
struct FooVtable {
desctructor: fn(*mut ()),
size: usize,
align: usize,
method: fn(*const ()) -> String,
}
fn call_method_on_u8(x: *const()) -> String {
let byte: &u8 = unsafe{ &*(x as *const u8)};
byte.method()
}

fn call_method_on_string(x: *const()) -> String {
let string: &String = unsafe {&*(x as *const String)};
string.method()
}

static Foo_for_u8_vtable: FooVtable = FootVtable {
destructor:/* a destructor*/,
size: 1,
align: 1,
method: call_method_on_u8 as fn(*const()) -> String
};

static Foo_for_string_vtable: FooVtable = FooVtable {
desctructor: /* a desctructor*/,
size: 24,
align: 8,
method: call_method_on_string as fn(*const()) -> String
};

这样之后,编译器就可以把let p = &x as &Foo做如下的转换:

1
2
3
4
5
// let p = &x as &Foo
let p = TraitObject {
data: &a,
vtable: &Foo_for_u8_vtable
};

调用Trait Object对应的方法也就转换成了:

1
2
// p.method()
p.vtable.method(p.data)

Object Safe

并不是所有的Trait都可以有相应的Trait Object的,一个Trait如果想要构造出一个Trait Object,那么这个Trait必须时Object Safe的,那什么是Object Safe呢?它必须满足以下两个条件:

  1. trait没有Self: Sized约束

  2. trait中所有的方法都是object safe的

    trait中所有的方法都是Object Safe的是指:

    • 不能有任何的类型参数,即方法不能是泛型的

    • 方式中没有Self参数

    一个Trait 只是描述了一个类型的公共行为,它并不知道这个类型的具体实现的大小,也就是说实现这些公共行为的具体类型大小各异,并不受拘束,那么如果我们给一个Trait加上Self: Sized约束,这就是告诉我们,这个Trait所描述的具体类型大小在编译器就知道了,即它的大小是一个常量,而这与Trait Object是相违背的。所以如果我们像下面一样定义一个Trait,我们是没有办法构造出一个Trait Object的。

    1
    2
    3
    4
    5
    6
    7
    trait Foo where Self: Sized {
    fn foo(&self);
    }

    ...

    // let p = &x as &Foo; // error

    有时候我们只是想让某些方法不出现在vtable中,而不是构造不出Trait Object,这个时候只要给对应的方法加上Self: Sized约束即可。即:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    trait Foo {
    fn foo(&self);
    fn not_in_vtable(&self) where Self: Sized;
    }

    ...

    let p = &x as &Foo;
    p.foo(); // corrent
    // p.not_in_vtable(); // error

    一个trait方法如果有一个Self参数会发生什么呢?拿Clone举个例子,

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    pub trait Clone {
    fn clone(&self) -> Self;

    fn clone_from(&mut self, source: &Self) { ... }
    }

    ...

    let p = &x as &Clone;
    let y = p.clone();

    首先由x得到了一个Trait Object,这个时候编译器已经不知道x原来的类型是什么了,只是只要由p可以调用一些列Clone所定义的方法,而接下来又要调用clone这个函数来复制x的一个实例,这就让编译器犯难了,即然它都不知道原先的类型是啥,又何谈复制?所以Rust就规定这种情况下,Trait不是Object Safe的,也就不能构造出Trait Object

    如果一个trait方法没有self参数,即为一个静态方法,那么很显然静态方法是没有办法放进vtable中的,所以静态方法也不是object safe的

    如果trait方法中有类型参数,比如:

    1
    2
    3
    trait Foo {
    fn foo<T>(&self, t: T);
    }

    由于泛型参数是在编译期自动展开的,如果要讲泛型函数放到vtable中,就必须把所有的foo版本都赛到虚函数表中,这个就太为难Rust编译器了,所以Rust规定,如果有类型参数,那么方法也不是Object Safe的。