Basically the idea is to use Rust as if it were C, kind of like how people program in C with C++ compilers and try to minimise their use of the C++ standard library or it's advanced features.
The rules of Crust are:
Every function is unsafe
No references, only raw pointers
No std, but libc is allowed
Pointers are mutable by default
Bound to annoy Rust purists with a thing about memory safety:) But I picked up a lot of useful tips there. Useful for anyone who wants to get into embedded Rust for example.
One thing that bothered me about Crust is that Tsoding was using rustc from a Makefile to build it. After futzing around and a little help from an AI buddy I managed to get a minimal Crust program built with Cargo and a build.rs.
Then the problem was that cargo test did not work. After more futzing around I fixed that. And below is how it looks. If anyone has any suggestions that would be great.
// main.rs - A minimal Crust program with tests
//
#![cfg_attr(not(test), no_std)]
#![cfg_attr(not(test), no_main)]
extern crate libc;
#[cfg(not(test))]
use core::panic::PanicInfo;
#[cfg(not(test))]
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
#[cfg(not(test))]
#[unsafe(no_mangle)]
pub extern "C" fn main() -> i32 {
let s = b"Hello world!\n\0";
unsafe {
libc::printf(s.as_ptr() as *const i8);
}
0
}
#[allow(unused)]
fn add(x: u32, y: u32) -> u32 {
let r = x + y;
let s = "%d + %d = %d\n\0";
unsafe {
libc::printf(s.as_ptr() as *const i8, x, y, r);
}
r
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
// build.rs - Required for building Crust programs (On Mac OS at least)
fn main() {
// Link against the system C library on macOS
println!("cargo:rustc-link-lib=System");
}
Can you tell us a bit about the motivation? (I generally watch no videos, and have currently no time available to do watching).
My guess would be to help novices learn unsafe pointer stuff, avoiding coming in contact with real C. Or creating tiny programs with libc, avoiding the megabyte which Rust typically adds with static linking. But note, Zig is already a quite successful C replacement, Carbon is something like a nicer C++ frontend, and Nim is something I used for nearly ten years.
Hmm...well, the thing is Tsoding does not pitch his YT channel as instructional or educational or even useful. No, it's live stream coding for entertainment. He has done all kind of weird and wonderful things all in all kind of languages. C, C++, Zig, Jai, Swift, Ada, shader languages, and much more. Typically languages and things he knows little about before he starts, it's an exploration. He does it with great style and humour. He always strives to illuminate bloat and inefficiency. My favourites have been a series on building and training a neural network from scratch and writing a compiler for his own language, Porth.
Many people waste many hours of their lives watching football or ice hockey or whatever. Me I keep half an eye on things like this whilst I'm doing something else.
In this case he's just having fun. But it turns out there is a lot of useful nuggets of information in there.
As for Zig, Carbon, Nim etc, I'm sure they are just fine. Just that I had never heard of them when I stumbled across Rust 5 years ago and nothing has lured me away from it yet. Actually I'm actively resistant to YAFL (Yet Another F...... Language) after having been through so many over the decades.
Anyway, after Crust I'm more encouraged to use Rust on my micro-controllers.
#![no_std]
#![no_main]
use core::ffi::*;
use defer_lite::defer;
c_import::c_import!(
"<stdio.h>",
"<stdlib.h>",
"<cairo.h>",
"$pkg-config --cflags cairo",
"--link cairo",
"--link c"
);
#[no_mangle]
pub extern "C" fn main(argc: c_int, argv: *const *const c_char) -> c_int {
unsafe {
let args = core::slice::from_raw_parts(argv, argc as _);
for (i, arg) in args.iter().enumerate() {
printf("arg %d is %s\n\0".as_ptr() as _, i, *arg);
}
let version = cairo_version();
let msg_len = snprintf(core::ptr::null_mut(), 0, "Cairo version is: %d\n\0".as_ptr() as _, version);
let buf: *mut c_char = malloc(msg_len as _) as _;
defer!(free(buf as _));
snprintf(buf, msg_len as _, "Cairo version is: %d\n\0".as_ptr() as _, version);
printf("%s\n\0".as_ptr() as _, buf);
}
0
}
#[panic_handler]
fn ph(_: &core::panic::PanicInfo) -> ! {
loop {}
}
This reminds me about a joke-blog post I have read some time ago, about a guy who during a job interview was asked to solve some very easy problem in Rust (maybe it was fizz-buzz - I can't remember the details). And instead just writing simple solution, he started writing minimal no-std/no-core/no-main binary while constantly talking over the interviewer. And if I recall correctly the punch-line of this blog was that after he finished his "rant" the interviewer asked him if it could be done simpler, to which he stated that yes, you could pre-calculate the solution to the question at build-time in a build script.
I cannot remember whose blog it was, and cannot find a link to it. But if anyone knows what I am talking about and has a link to it I would gladly read it one more time, because I gave me a pretty good laugh the first time.
This has the potential to cause UB because of alignment restrictions, depending on the definition of T and the details of the malloc implementation on your system. Miri will only complain about this if it actually observes malloc giving you an incorrectly-aligned pointer.
In practice, mallocprobably works with chunks larger than the biggest alignment requirement you'll run across, so it's probably fine for experimentation like this. But best to avoid things like #[repr(align(...))] annotations.
My compiler tells me these are not "methods" they are "associated items". For example the warning message:
warning: associated items `new`, `push`, `len`, `len_1`, `is_empty`, and `capacity` are never used
If an "associated" item takes a raw pointer to self is it really a "method"? It is a common thing in C to have a struct and then a lot of functions that take a pointer to struct as first parameter. Why should I not do that in Crust.
I guess I could define those "methods" outside of an impl block as C style free functions (Which is what Tsoding ended up doing) But then I have to add a type parameter declaration. This:
I don't have much of a problem with namespacing, it's just letting you avoid repeating a prefix in a context where it would be redundant most of the time, and impl blocks are even less offensive. The purest terrible use of Rust as if it were C would be to use macros instead of generics too - but there are some evils that we must not stand silently by as they happen.