分类

Rust 语言受限的地方

2020-04-18 10:03 rust

最近半年陆续写了很多的 Rust 代码,跟别的语言相比,它有些地方还是蛮受限的,在表达 一个功能时需要各种弯弯绕,下面就列举几个方面的问题。

struct/union 不支持嵌套

类型嵌套可以将关联的数据结构放在一起,这个在C语言里是很常用的。像内核提供的系统 调用,比如 bpf() 以及 io_uring_setup(),都有大量的使用。它们都是定义了一堆的 操作指令(operation-code, op-code),以及一个公用的 union 联合体,跟据不同的 操作指令来解析 union 为不同的结构。尤其是 bpf() 系统调用,使用一个接口就实现了 内核 BPF 虚拟机的所有操作,相当的强大和复杂。

在使用Rust为类似这样的 C 接口做绑定时,会很麻烦。需要先将嵌套的结构体以及 union 联合体拆成独立的片段,然后再依次组装到一起,这样写的代码,其可读性并不好,也更难 维护。

/// IO submission data structure (Submission Queue Entry)
#[repr(C)]
#[derive(Copy, Clone)]
pub struct io_uring_sqe_t {
    /// type of operation for this sqe
    pub opcode: IOURING_OP,

    /// IOSQE_ flags
    pub flags: u8,

    /// ioprio for the request
    pub ioprio: u16,

    /// file descriptor to do IO on
    pub fd: i32,

    pub file_off: io_uring_sqe_file_off_t,

    pub buf_addr: io_uring_sqe_buf_addr_t,

    /// buffer size or number of iovecs
    pub len: u32,

    pub other_flags: io_uring_sqe_other_flags_t,

    /// data to be passed back at completion time
    pub user_data: u64,

    pub opt_buf: io_uring_sqe_opt_buf_t,
}

以上代码片段定义了 IO_Uring 里的 Submition Queue Entry,这里需要额外定义4个 结构体/联合体;而在原先的内核头文件中,它们是放在了一起定义的,很明确:

/*
 * IO submission data structure (Submission Queue Entry)
 */
struct io_uring_sqe {
	__u8	opcode;		/* type of operation for this sqe */
	__u8	flags;		/* IOSQE_ flags */
	__u16	ioprio;		/* ioprio for the request */
	__s32	fd;		/* file descriptor to do IO on */
	union {
		__u64	off;	/* offset into file */
		__u64	addr2;
	};
	union {
		__u64	addr;	/* pointer to buffer or iovecs */
		__u64	splice_off_in;
	};
	__u32	len;		/* buffer size or number of iovecs */
	union {
		__kernel_rwf_t	rw_flags;
		__u32		fsync_flags;
		__u16		poll_events;
		__u32		sync_range_flags;
		__u32		msg_flags;
		__u32		timeout_flags;
		__u32		accept_flags;
		__u32		cancel_flags;
		__u32		open_flags;
		__u32		statx_flags;
		__u32		fadvise_advice;
		__u32		splice_flags;
	};
	__u64	user_data;	/* data to be passed back at completion time */
	union {
		struct {
			/* pack this to avoid bogus arm OABI complaints */
			union {
				/* index into fixed buffers, if used */
				__u16	buf_index;
				/* for grouped buffer selection */
				__u16	buf_group;
			} __attribute__((packed));
			/* personality to use, if used */
			__u16	personality;
			__s32	splice_fd_in;
		};
		__u64	__pad2[3];
	};
};

这样的接口是很难维护的。

不支持类型继承(subtyping)

Rust 本身支持基于 Trait 的继承,比如:

pub trait Animal {
}

pub trait Cat: Animal {
}

但是,与 java/python 等相比的话,并不支持基于结构体的继承,就像下面的写法。

pub struct Animal {
}

pub struct Cat: Animal {
}

社区里更推荐使用组合的写法,即所谓的“组合优于继承”。

不支持尾递归优化(Tail Call Optimization)

尾递归调用对于一些算法的实现是更直接的,更易理解。一些编译器和语言支持对这样的 尾递归调用做优化,在生成的代码中,并不会创建新的函数调用栈(function frame), 其栈大小并不会增加,而只是更改寄存器的值,跳转(jump)到函数开始处。 但 Rust 并不支持对其优化,这个不是编译器的问题,rustc 的后端是 LLVM, LLVM 本身是支持尾递归优化的。

不支持 yield

这种语法是所谓的生成器模式(Generator),这个在 python 中很容易实现。有些算法 基于这个来实现的话,是很直接的,比如很出名的 Fabonacci 算法:

def fibonacci_init():
    a, b = 0, 1
    yield b
    while True:
        a, b = b, a+b
        yield b

fib = fibonacci_init()
for _i in range(100):
    print(next(fib))

但如果要在 Rust 中实现类似的操作,需要基于 Iterator trait 实现。

#[derive(Debug)]
pub struct Fibonacci {
    a: u64,
    b: u64,
}

impl Fibonacci {
    fn new() -> Fibonacci {
        Fibonacci { a: 0, b: 1 }
    }
}

impl Iterator for Fibonacci {
    type Item = u64;
    fn next(&mut self) -> Option<Self::Item> {
        let tmp = self.b;
        self.b += self.a;
        self.a = tmp;
        Some(tmp)
    }
}

fn main() {
    let fib = Fibonacci::new();
    for n in fib.take(50) {
        println!("> {}", n);
    }
}