Hey!

I’m a professional software engineer with several years of experience using Rust. Unfortunately I don’t really have the time to contribute to Lemmy directly myself, but I love teaching other people Rust so if:

  • You are curious about Rust and why you should even learn it
  • You are trying to learn Rust but maybe having a hard time
  • You are wondering where to start
  • You ran into some specific issue

… or anything to do with Rust really, then feel free to ask in the comments or shoot me a PM 🙂

  • @BehindTheBarrier@programming.dev
    link
    fedilink
    English
    23 months ago

    Sorry, but a long and slightly complicated question, for a hypotetical case.

    I wanted to serve pages in my blog. The blog doesn’t actually exist yet (but works locally, need to find out how I can safely host it later…), but lets assume it becomes viral, and by viral i mean the entire internet has decided to use it. And they are all crazy picky about loading times…

    I haven’t figued out the structure of the Page objects yet, but for the question they can be like the last question:

    #[derive(Clone)]
    pub struct Page<'a> {
        pub title: &'a str,
        pub endpoint: &'a str,
    }
    

    I wanted to create a HashMap that held all my pages, and when I updated a source file, the a thread would replace that page in the mapping. It’s rather trivial of a problem really. I didnt find out if I could update a mapping from a thread, so I decided to make each value something that could hould a page and have the page object replaced on demand. It made somewhat sense since I don’t need to delete a page.

    There is a trivial solution. And it’s just to have each HashMap value be a RwLock with an Arc holding my large string. No lagre string copies, Arc make it shared, and RwLock is fine since any number of readers can exist. Only when writing is the readers locked. Good enough really.

    But I heard about DoubleBuffers, and though, why can’t I have a AtomicPointer to my data that always exist? Some work later and I had something holding an AtomicPointer with a reference to an Arc with my Page type. But it didn’t work. It actually failed rather confusingly. It crashed as I was trying to read the title on my Page object after getting it from the Arc. It wasn’t even any thread stuff going on, reading once works, the next time it crashed.

    struct SharedPointer<T> {
        data: AtomicPtr<Arc<T>>,
    }
    
    impl<T> SharedPointer<T> {
        pub fn new(initial_value: T) -> SharedPointer<T> {
            SharedPointer {
                data: AtomicPtr::new(&mut Arc::new(initial_value)),
            }
        }
    
        pub fn read(&self) -> Arc<T> {
            unsafe { self.data.load(Relaxed).read_unaligned() }.clone()
        }
    
        pub fn swap(&self, new_value: T) {
            self.data.store(&mut Arc::new(new_value), Relaxed)
        }
    }
    
    #[test]
    pub fn test_swapping_works_2() {
        let page2: Page = Page::new("test2", "/test2");
        let page: Page = Page::new("test", "/test");
        let entry: SharedPointer<Page> = SharedPointer::new(page.clone());
    
        let mut value = entry.read();
    
        assert_eq!(value.title, page.title);
        value = entry.read();
        assert_eq!(value.title, page.title);
    
        entry.swap(page2.clone());
    
        let value2 = entry.read();
        assert_eq!(value2.title, page2.title);
        assert_eq!(value.title, page.title);
    }
    

    This has undefined behavior, which isn’t too surprising since I don’t understand pointers that much… and I’m actually calling unsafe code. I have heard it can produce unexpected error outside it’s block. I’m just surprised it works a little. This code sometimes fails the second assert with an empty string, crashes with access violation, or one time it gave me a comparison where some of it was lots of question marks! My best understanding is that my Page or it’s content is moved or deallocated, but odd that my Arc seems perfectly fine. I just don’t see the connection between the pointer and Arcs content causing a crash.

    I may just be doing the entire thing wrong, so sticking with RwLock is much better and safer since there is no unsafe code. But I seek to know why this is so bad in the first place. What is wrong here, and is there a remedy? Or is it just fundamentally wrong?

    • @SorteKaninOPA
      link
      English
      23 months ago

      I wanted to serve pages in my blog. The blog doesn’t actually exist yet (but works locally, need to find out how I can safely host it later…), but lets assume it becomes viral, and by viral i mean the entire internet has decided to use it. And they are all crazy picky about loading times…

      Of course it depends if doing this kind of optimization work is your goal but… if you just want a blog and you want it to be fast (even with many visitors, but perhaps not the entire internet…), I would say make a static web server that just serves the blog pages directly from &'static strs and predefine all blog posts ahead of time. For example, you could write all your blog posts in HTML in separate files and include them into your code at compile time.

      You’d need to recompile your code with new blog post entries in order to update your blog… but like how often are you gonna add to your blog? Recompiling and redeploying the blog server wouldn’t be an issue I imagine. That’s how I would do it if I wanted a fast and simple blog.

      Also general software development wisdom says “don’t code for the future” aka YAGNI - you aren’t gonna need it. I mean, sorry, but chances are the whole internet will not be crazy about visiting your blog so probably don’t worry about it that much 😅. But it is a good learning thing to consider I guess.

      #[derive(Clone)]
      pub struct Page<'a> {
         pub title: &'a str,
         pub endpoint: &'a str,
      }
      

      I’m a little confused about the use of the word “endpoint” here - that usually indicates an API endpoint to me but I would think it would be the post contents instead? But maybe I’m just too hung up on the word choice.

      I wanted to create a HashMap that held all my pages, and when I updated a source file, the a thread would replace that page in the mapping.

      To me, this sounds like you want to dynamically (i.e. at runtime, while the server is running) keep track of which blog entry files exist and keep a shared hashmap of all the blog files.

      So there’s multiple things with that:

      1. You’d need to dynamically allocate the storage for the files on the heap as you load them in memory, so they’d need to be String or an Arc<str> if you only need to load it in once and not change it. Since you don’t know at compile-time how big the blog posts are.
      2. As you note, you’d need a way to share read-only references to the hashmap while also providing a way to add/remove entries to it at runtime. This requires some kind of lock-syncing like Mutex or RwLock, yes.

      why can’t I have a AtomicPointer to my data that always exist?

      Does it always exist though? The way you talk about it now sounds like it’s loaded at runtime, so it may or may not exist. I think I’d need to see more concrete code to know.

      and I’m actually calling unsafe code. I have heard it can produce unexpected error outside it’s block.

      Yes, indeed. Safe code must never produce undefined behaviour, but safe code assumes that all unsafe blocks does the correct thing. For instance, safe code will always assume a &str contains UTF-8 encoded data but some unsafe code may have earlier changed the data inside of it to be some random data. That will break the safe code that makes the assumption! But it’s not the safe’s code fault.

      Unsafe in general is a very sharp tool and you should be careful. In the best case, your program crashes. In worse cases, your program continues with garbage data and slowly corrupts more and more. In the even worse case, your program almost always works but rarely produces undefined behaviour that is extremely hard to track down. You could also accidentally introduce security vulnerabilities even if your code works correctly most of the time.

      In general, I would advise you to avoid unsafe like the plague unless you really need it. A hypothetical optimization is certainly not such a case. If you really want to use unsafe, you definitely need to carefully peruse the Rustonomicon first.

      In your specific case, the problem is (of course) with the unsafe block:

      unsafe { self.data.load(Relaxed).read_unaligned() }.clone()

      So what is this doing? Well self.data.load(Relaxed) returns a *mut Arc<T> but it is only using safe code so the problem must be with the read_unaligned call. This makes sense, obtaining a raw pointer is fine, it’s only using it that may be unsafe.

      If we check the docs for the read_unaligned function, it says:

      Reads the value from self without moving it. This leaves the memory in self unchanged.

      Here “self” is referring to the *mut Arc<T> pointer. So this says that it reads the Arc<T> directly from the memory pointed to by the pointer.

      Why is this a problem? It’s a problem because Arc<T> is a reference-counted pointer, but you’ve just made one without increasing the reference count! So the Arc believes there are n references but in fact there are n + 1 references! This is bad! Once the Arc is dropped, it will decrease the reference count by 1. If the reference count is 0, it will drop the underlying data (the T).

      So let’s say you get into this situation with 2 Arcs but actually the reference count is 1. The first one will drop and will try to free the memory since the reference count is now 0. The second one will drop at some later time and try to update the reference count but it’s writing into memory that has been freed so it will probably get a segmentation fault. If it doesn’t get the segfault, it will get a problem once it tries to free the memory since it’s already been free. Double free is bad!

      So yea that’s why it probably works once (first arc gets dropped) but not twice (second arc gets a bad experience).

      • @BehindTheBarrier@programming.dev
        link
        fedilink
        English
        2
        edit-2
        3 months ago

        Ah, so I’m actually cheating with the pointer reading, i’m actually making a clone of Arc<T> without using the clone()… And then dropping it to kill the data. I had assumed it just gave me that object so I could use it. I saw other double buffer implementations (aka write one place, read from another palce, and then swap them safely) use arrays with double values, but I wasn’t much of a fan of that. There is some other ideas of lock free swapping, using index and options, but it seemed less clean. So RwLock is simplest.

        And yeah, if I wanted a simple blog, single files or const strings would do. But that is boring! I mentioned in the other reply, but it’s purely for fun and learning. And then it needs all the bells and whistles. Writing html is awful, so I write markdown files and use a crate to convert it to html, and along the way replace image links with lazy loading versions that don’t load until scrolled down to. Why, because I can! Now it just loads from files but if I bother later i’ll cache them in memory and add file watching to replace the cached version. Aka an idea of the issue here.