Hacker News new | ask | show | jobs
by notpopcorn 1600 days ago
Without any unsafe code this is simply:

    let role = Role {
        name: "basic",
        flag: 1,
        disabled: false,
    };
The language tries to prevent you from interacting with a `Role` object that's not fully initialized. `mem::zero()` could work, but then you'll have to turn the `&'static str` into an `Option<&'static str>` or a raw pointer, to indicate that it might be null. You could also add `#[derive(Default)]` to the struct, to automatically get a `Role::default()` function to create a `Role` with and then modify the fields afterwards, if you want to set the fields in separate statements for some reason:

    let mut role = Role::default();
    role.name = "basic";
    role.flag = 1;
    role.disabled = false;
And even with `MaybeUninit` you can initialize the whole struct (without `unsafe`!) with `MaybeUninit::write`. It's just that partially initializing something is hard to get right, which is the point of the article I guess. But I wonder how commonly you would really want that, as it easily leads to mistakes.
4 comments

Exactly this. I'm not sure what the author's practical goal is with that code. He rejects #[repr(C)] a few times, so it's not FFI.

Yes, working with uninitialized memory is tedious. But that isn't something you ever have to do. If you're translating some C to Rust, write it using Rust idioms, instead of trying to preserve every call to malloc/free and every access to uninitialized memory.

The point is to make points in a simple environment.

Anything small enough to clearly make points about unsafe Rust is almost certainly small enough to be done in safe Rust, defeating the purpose.

If it’s too big for an example, it’s almost certainly too big for “trust me, I know this is safe”.
I don't see how you can make this claim. An example is meant to be understandable after a cursory introduction and communicate an idea to the reader. If it takes me a day to understand the "example", it's not useful as an example, but real problems often take that long to completely understand in any language.
Also, the C version should really look like this:

    const struct role r = {
        .name = "basic",
        .flag = 1,
        .disabled = false,
    };
(of course this doesn't give you uninitialized memory in case new items are added to the struct, but why would one ever want that?)
A much better way to do partial initialization is by splitting up the struct into multiple parts. This can be easily done in safe rust with Option, or MaybeUninit if you're really desperate for performance.
When dealing with unix syscalls, you actually sometimes need to pass structs that aren't fully initialized, or are initialized to zero with the exception of some fields. The quintessential example is the sigaction struct.
Another good example is Win32, in many cases only the length is initialized and the API does the rest, this allows them to change the ABI across versions without impacting the caller.
That should work perfectly with multiple structs. Define

    #[repr("C")]
    struct MyThing {
        length: usize,
        data: MaybeUninit<MyInner>
    }
Then initialize with sizeof(data) and MaybeUninit::uninitialized(). When the call is complete, assume_init() and access the fields of the result struct as normal.
I see, thanks.
> The language tries to prevent you from interacting with a `Role` object that's not fully initialized.

I remember that after reading the Rust book, one of the first things I tried to do was to load a struct from a file. Like pseudocode:

    struct MY_STRUCT my_struct;
    read(file, &my_struct, sizeof(my_struct));
2 lines of code.... It should be simple, right? RiGhT?! Well, the first stackoverflow answer involved unsafe and a bunch of other stuff I didn't understand. And also I thought that as a beginner I shouldn't start fiddling with unsafe right away (otherwise, what's the point? I'm trying to move away from C). Then I learned that structs are not laid out as declared (OMG!), etc.

So, thinking this went beyond my skills I left it aside and tried to make a nice console logging library for my projects. It should be simple! I tried to create a variadic function and you can guess how it went.

I am out of luck with Rust.

>It should be simple, right?

Well, no. If you want to have memory safe subset, you absolutely cannot initialize structs with random bag of bytes in general case. C let's you cut corners here, but in Rust you need to implement (de)serializing logic (no need for unsafe).

>Then I learned that structs are not laid out as declared (OMG!), etc. >It should be simple! I tried to create a variadic function and you can guess how it went.

This is only surprising if you have this weird assumption that things should work like they do in C + some extra.

> in Rust you need to implement (de)serializing logic

Hmm. Well that sucks. It might be slower than reading into a struct directly (does more IO calls, there is more code to execute, probably uses more memory). My focus is on embedded applications, so "the less, the better".

> no need for unsafe

Can you point out an example on reading a bunch of structs from a file? Without std? Now I'm really curious.

> you have this weird assumption

You are probably right. I intended to use Rust in embedded as I said before so I thought a system language like Rust could fit. But you are right and the more I read (docs and comments like yours) I realize it's not C, and Rust may require extra steps and resources to achieve the same things I can do in C. With consequential hit on performance/code size/whatever, that it's a thing to consider seriously in constrained environments.

Rust works great in embedded. You do have to learn some things about how to get binary sizes down, depending on how resource constrained you are. If you're on a device that's running Linux, you probably don't need to think about it at all. If you're on a device and writing 100% of the code yourself, you may have to watch out for certain things, like formatting code, that can add a bunch of bloat, and choose alternatives.

At work we have a de novo microkernel we're using for the firmware of a few things inside of our product. A recent build we did to check on binary size of OS + 5 simple tasks using 22kb of flash and 3.5 kb of RAM. Those tasks are all separately compiled programs, it all gets put together on one single image to flash.

If you're talking 8 bit micros, you run into platform support issues before you even get to the binary size stuff, but if you're on Arm, even the low end, size is not the primary issue when it comes to doing Rust projects.

>Can you point out an example on reading a bunch of structs from a file? Without std? Now I'm really curious.

Not really, but here is really simple example:

  use std::fs::File;
  use std::io::Read;
  
  #[derive(Debug)]
  struct Point {
      x: i32,
      y: i32,
  }
  
  fn bytes2point(buf: [u8; 8]) -> Point {
      Point {
          // Slices to arrays is a bit unwieldy...
          x: i32::from_le_bytes(buf[..4].try_into().unwrap()),
          y: i32::from_le_bytes(buf[4..].try_into().unwrap()),
      }
  }
  
  fn main() {
      let mut buf: [u8; 8] = [0; 8];
      let mut file: File = File::open("data.txt").unwrap();
      Read::read(&mut file, &mut buf).unwrap();
      let point: Point = bytes2point(buf);
      println!("{:?}", point);
  }
It doesn't do any extra i/o compared to C. Std is only used for filesystem access and printing. It does require extra buffer.
Thank you.

I see. I'm looking now at how to load DOOM WAD files in Rust (https://github.com/bjt0/rs_wad/blob/master/src/wad.rs#L105) as an example, and I see it uses your method (even if it seems to store each piece in individual variables to then copy them into the struct).

It's not as straightforward as one would expect (as you say, it requires an extra buffer and parsing, but no extra IO). I surely wouldn't get to the solution by myself.

I'd be surprised if you see that Rust code is significantly slower than equivalent C code; this isn't how it pans out in most benchmarks. The killer for Rust is executable binary size, which even with no-std and other tricks can still balloon to an enormous size.