At least 4 bugs in the above C code!
int fd
: fd
can be negative or zerovoid *buf
: buf
can be nullremain = count
: remain
can be negative due to unmatched typebuf = (char *)buf + ret
: ret
can be negative, causing out-of-bound accessWhat features of Rust can help?
Same thing implemented in Rust:
fn someos_file_write(fd: FileDescriptor, buf: &mut [u8]) -> SomeOsResult<usize> {
let count = buf.len();
let mut written = 0usize;
while written < count {
let buf_slice = &mut buf[written..min(written + FS_BUF_SIZE, count)];
let n = ipc_call(buf_slice)?;
written += n;
if n < buf_slice.len() {
break;
}
}
Ok(written)
}
What if forgot min(written + FS_BUF_SIZE, count)
?
let buf_slice = &mut buf[written..written + FS_BUF_SIZE];
If written + FS_BUF_SIZE
is larger than buf.len()
, it'll panic:
thread 'main' panicked at 'range end index 1024 out of range for slice of length 512', src/main.rs:23:30
A bad example in C:
void usb_request_async(int req, void *buffer, void (*callback)(int, void *)) {
global_urb->req = req;
global_urb->buffer = buffer;
global_urb->callback = callback;
usb_send_urb_async(global_urb);
}
void usb_request_cb(int req, void *buffer) { /* access buffer */ }
void func() {
unsigned char buf[4096];
usb_request_async(USB_REQ_GET_XXX, buf, usb_request_cb);
}
What features of Rust can help?
Try the same in Rust (consider only immutable buffer for simplicity):
fn usb_request_async<'a>(
req: UsbRequest,
buffer: &'a [u8],
callback: Box<dyn Fn(UsbRequest, &[u8]) + Send + 'static>,
) {
std::thread::spawn(move || { // simulate async usb request callback
callback(req, buffer);
});
}
error[E0759]: `buffer` has lifetime `'a` but it needs to satisfy a `'static` lifetime requirement
Change a little bit according to the error message:
fn usb_request_async(
req: UsbRequest,
buffer: &'static [u8],
callback: Box<dyn Fn(UsbRequest, &[u8]) + Send + 'static>,
) {
std::thread::spawn(move || { // simulate async usb request callback
callback(req, buffer);
});
}
Then use usb_request_async
:
fn usb_request_cb(req: UsbRequest, buffer: &[u8]) {
println!("{:?}", buffer);
}
fn func() {
let buffer: [u8; 1024] = [0; 1024];
usb_request_async(UsbRequest::GetXxx, &buffer, Box::new(usb_request_cb));
}
error[E0597]: `buffer` does not live long enough
Possibly a better design:
struct Urb {
req: UsbRequest,
buffer: Vec<u8>,
callback: Box<dyn Fn(UsbRequest, &[u8]) + Send + 'static>,
}
impl Urb {
fn new(req: UsbRequest, buf_size: usize,
callback: impl Fn(UsbRequest, &[u8]) + Send + 'static) -> Self {
Self { req, buffer: vec![0; buf_size], callback: Box::new(callback) }
}
}
fn usb_send_urb_async(urb: Urb) {
std::thread::spawn(move || { // simulate async usb request callback
let cb = urb.callback;
cb(urb.req, &urb.buffer);
});
}
fn usb_request_cb(req: UsbRequest, buffer: &[u8]) {
println!("{:?}", buffer);
}
fn func() {
let urb = Urb::new(UsbRequest::GetXxx, 1024, usb_request_cb);
usb_send_urb_async(urb);
}
Features of Rust to prevent from data races:
Global static variables can't be modified safely
static mut GLOBAL_INT: u32 = 1;
fn func() {
// bad practice
std::thread::spawn(|| unsafe { GLOBAL_INT = 3 });
unsafe { GLOBAL_INT = 2 }
}
Sync
// static GLOBAL_INT_BAD: RefCell<u32> = RefCell::new(1);
lazy_static! {
static ref GLOBAL_INT: Mutex<u32> = Mutex::new(1);
}
fn func() {
std::thread::spawn(|| *GLOBAL_INT.lock().unwrap() = 3);
*GLOBAL_INT.lock().unwrap() = 2;
}
Variables that are sent across threads must impl Send
fn func() {
let shared_int = Arc::new(Mutex::new(1));
let shared_int_clone = shared_int.clone();
std::thread::spawn(move || *shared_int_clone.lock().unwrap() = 3);
*shared_int.lock().unwrap() = 2;
}
Can Rust express everything that C can?
In theory, YES, both are turing-complete.
In practice, ALMOST, with some unsafe
code. When you can't implement some logic in Rust, you probably should rethink the design.
Example for booting:
#[no_mangle]
#[link_section = ".text.boot"]
extern "C" fn _start_rust() -> ! {
#[link_section = ".data.boot"]
static START: spin::Once<()> = spin::Once::new();
START.call_once(|| {
clear_bss();
memory::create_boot_pt();
});
memory::enable_mmu();
unsafe { _start_kernel() }
}
Example for driver:
fn handle_irq(&self) {
loop {
let irqstat = self.read_cpu_reg(GICC_IAR);
let irqnr = irqstat & GICC_IAR_INT_ID_MASK;
match irqnr {
0..=15 => crate::irq::handle_inter_processor(irqnr),
16..=31 => crate::irq::handle_local(irqnr - 16),
32..=1020 => crate::irq::handle_shared(irqnr - 32),
_ => break,
}
self.write_cpu_reg(GICC_EOIR, irqstat);
}
}
Much better than C:
Compared to C++:
core
and alloc
crates without need for std
Call C from Rust:
extern "C" {
fn some_function_in_c(buf: *mut u8, n: usize) -> usize;
}
fn func() {
let mut buf = [0u8; 1024];
let buf_raw_ptr = &mut buf[0] as *mut u8;
let ret = unsafe { some_function_in_c(buf_raw_ptr, buf.len()) };
}
Call Rust from C:
#[no_mangle]
extern "C" fn some_function_in_rust(buf: *mut u8, n: usize) -> usize {
let buf = unsafe { core::slice::from_raw_parts_mut(buf, n) };
println!("{:?}", buf); buf.len()
}
extern size_t some_function_in_rust(unsigned char *buf, size_t n);
void func() {
unsigned char buf[1024];
some_function_in_rust(buf, 1024);
}
According to The Computer Language Benchmarks Game, Rust achieves same or even better performance than C and C++.
Features that make Rust fast:
Open source projects (re)written in Rust that're proved to be fast(er):
Tools:
Example for dependency management:
[dependencies]
spin = "0.7.0"
buddy_system_allocator = { git = "https://github.com/richardchien/buddy_system_allocator.git" }
log = "0.4.0"
lazy_static = { version = "1.4.0", features = ["spin_no_std"] }
bitflags = "1.2.1"
[target.'cfg(target_arch = "aarch64")'.dependencies]
aarch64 = { git = "https://github.com/richardchien/aarch64", rev = "5dc2a13" }
Debug
trait for easy debugging:
#[repr(u8)]
#[derive(Debug)]
enum IntType {
SyncEl1t = 1,
IrqEl1t = 2,
// ...
}
#[no_mangle]
extern "C" fn _handle_interrupt(int_type: IntType) {
println!("Interrupt occurred, type: {:?}", int_type);
}
std
Special thanks to Alex Chi for reviewing the slides.