Announcement

Collapse
No announcement yet.

Fedora 38 Looks To Shift RPM To Sequoia, A Rust-Based OpenPGP Parser

Collapse
X
 
  • Filter
  • Time
  • Show
Clear All
new posts

  • #31
    Originally posted by lowflyer View Post

    You delivered *exactly* what I expected: A hodgepodge of snippets and buzzwords. Not a tiny piece of working code. Not even the rusty snippets. So I created a working example for you. ("working example" has an exact, narrow definition. Look it up.) You need to click on the link. It opens the godbolt.org website where you can write and execute code without having to install the compiler.

    As I said before, your statement is wrong, you can have exceptions in threads and it does not crash. It prints the exact failure of the second thread while the others are executed normally. Also note that the C++ code looks strikingly similar to the rust snippets. No wonder, it does *exactly* the same thing.

    However, I was not able to get the rust snippet to work. (Yes, rust is likewise on godbolt.org) I guess you know a rust expert you can ask to help you make it work. Just a remark: the rustc compiler spits out this error message:
    Code:
    error[E0658]: use of unstable library feature 'future_join'
    Why do you bring up unfinished features in such discussions? it can backfire dramatically.

    The next topic is: "various features don't integrate well together". I'm eager to see you explaining it. But please, put in a little bit more effort on the coding side.
    Congratulations! In your rush to produce "working code" you added copious amounts of boilerplate that are strictly irrelevant to the problem at hand, but by the same token you managed to completely miss the point of the exercise. What you wrote is "working" code that catches exceptions in the same thread that throws them. That mostly works (except for corner cases). The point is that you can't catch exceptions across threads: in this case, in the Rust version the main thread handles errors that occur in the worker threads. In C++, you can use exceptions, or you can spawn a thread, but in the latter case, you can't use exceptions to report errors to the caller.

    The C++ version similar to the Rust one would look something like this:

    Code:
    auto process_stuff(const std::vector<float> & value, int index) -> float
    {
        return value.at(index);
    }​
    [...]
        try {
            auto fut1 = std::async(process_stuff, vec, 7);
            auto fut2 = std::async(process_stuff, vec, 12);
            auto fut3 = std::async(process_stuff, vec, 3);
            print_result(fut1.get());
            print_result(fut2.get());
            print_result(fut3.get());
        }
        catch (std::exception& e) {
            fmt::print("Error: {}\n", e.what());
        }​
    This of course crashes.

    It's not even really equivalent to the Rust version because even if exceptions propagated across threads, it still wouldn't really work because to my best knowledge, you can't write a try/catch block in C++ that would catch one error all the while letting the remaining threads finish.

    To your credit, you implicitly acknowledged this by writing your tortuous imitation of the Result type to prove that it "can" be done in C++. There are of course many problems with that, among others the fact that with your solution you can only return strings containing error messages and not actual, structured errors (that would need more boilerplate), but it's an excellent example of the fanboyism I was deriding: you obviously don't care about engineering outcomes or code provability, you care about making a point about "your" language which seems to sit somewhere alongside your football club. For every problem in C++, anyone can of course devise a labyrinthine solution to work around it in one particular case. That doesn't constitute any merit though.

    Comment


    • #32
      lowflyer I didn't read the C++ example you provided until now and it seems that you have creates Rust's Result in C++.

      TBH I have done the same thing a few yrs ago and back then my wish is that C++ supports an operator to make propagating my Result easier.

      Guess what, when I learn Rust, I figure out that it has exactly what I wanted back then and have '?' to propagate the Result and Option.

      And its trait system is much more sane than concept checking before C++20, well, even C++ 20 concept is still quite complex with all these copy assignable, move assignable, copy constructible, move constructible, convertible and etc.

      I also tried to implement concept checking in C++17 by myself using SFINAE and std::void_t, combined with utility and macros to ease the job.

      When C++20 comes out, I have a glanced at its coroutine and a blog written by one microsoft dev and it's just so complex... to even write on async function.

      And it's also not zero cost since it's always allocated on heap and the compiler is allowed to.optimize it out.

      Combined with complex language rules, the std::initializer_list, implicit integer conversion, have to use exception to signal error in constructor, plus memory bugs I frequently wrote, I decided that Rust is a better PL for me.

      This is not related to the discussion, but I just want to share it after seeing ur process_result.

      Comment


      • #33
        Originally posted by lowflyer View Post
        Look at it this way: I talk about "lifetime" and "reference counting" and you reply with "multithreading", "synchronization", "mutex" and "atomic". This smells an awful lot like another strawman argument.
        Well, apologies if I do sound like that, but my point is that lifetime in Rust is also coupled with its mutability.

        When you creates a reference to a variable, it can be either multi immutable references or one single mutable reference to prevent race condition.

        So the borrow checker not just ensures the variable is still alive when you attempt to borrow it, but also ensure the mutability rule is up hold.

        That can't be done by simple ref counting.

        Originally posted by lowflyer View Post
        In C++, multithreading is not a component of the compiler. It is implemented in a library. While in rust, it seems to be, an inherent component within the compiler.
        In rust, threading is in std, not the compiler and there are also third party crates for threading.

        The point is, borrow checker provides tool to prevent race condition.

        Originally posted by lowflyer View Post
        You praise the "no alias" as an additional optimization. While I doubt that this will always make the code run faster, I wonder what hoops you need to go through with rust if you *just need to do that* (access a mutable reference from multiple threads).
        You need to use Mutex, RwLock or other synchronization primitives.

        Originally posted by lowflyer View Post
        In rust, "references" seem to be literally *everywhere*. At least this is what I glean from multiple rust examples I've been shown here and in the past. Can you actually do something sensible in rust *without using references*? Or is this just used as another (strawman-) argument against other languages?
        Yes, you can declare a variable and access it, just like C/C++.

        There's also pointer for implementing primitives and expose a safe API.

        In terms of whether something sensible can be done without reference, well, this is just the same as pointers/references in C++.

        Pointers/references are just fundamental to programming.

        Originally posted by lowflyer View Post
        If the code to show the superiority of rust over *any* other language needs much more complication, then I fail to see the point of it.
        While its API indeed looks scary, it simply enables you to use any local variable from the current thread in the thread you will spawn **safely**.

        Originally posted by lowflyer View Post

        Cppcheck catches it:
        image.png
        (similar code on godbolt) Perhaps you argue "but rust has it built into the compiler"​. Well, cppcheck catches it *before* the compilation.
        Good to see that, though will it work if you use folly's vector, which can utilise realloc for POD types?

        Originally posted by lowflyer View Post
        Again, many references to multithreading, which is again sort-of evading the subject. If the strong point of rust is multithreading, you should not compare it with plain C++ object lifetimes.
        Borrow checker in rust is used to prevent undefined behaviors, including any memory access bug like use-after-free, concurrent modification to the same variable without synchronisation, etc.

        Originally posted by lowflyer View Post
        Despite that, I think there is nothing in what you explain above that cannot be done in C++ equally safe. I think that most of what you praise as "unique rust feature" is present in C++ and other languages already. It would be helpful if you could provide links to examples in godbolt-rust so we can really compare them. I would like to see examples for the concurrent execution of futures, the two futures in one thread with "borrowing" of variables and a thread::scope example. (I promise to put counter-examples in C++ for your rust-examples)
        I am currently on the bus, would do so after I get home.

        Originally posted by lowflyer View Post
        You get that wrong. C++ indeed has lifetime. Obviously we disagree. C++ example:
        Code:
        auto main() -&gt; int
        {
        int r; // lifetime of "r" starts *and* type is defined
        // impossible to define a reference of non-existing variable
        {
        int x{5};
        r = x; // impossible to redefine "r" as reference to "x"
        }
        } // lifetime of "r" ends​
        In rust on the other hand, lifetime is separate from the type. (at least that's how I see it)
        Code:
        fn main() {
        let r; // lifetime of "r" starts, but there's no type yet
        {
        let x = ".53";
        r = x; // type of "r" is "inferred"
        }
        println!("r: {}", r);
        } // lifetime of "r" ends
        That's because rust infer type backwards, that doesn't change the fact that lifetime is embedded in rust's typing.

        If you declare a struct S with field r as a reference to i32, to you would have to also declare a lifetime in that struct S.

        Every function API in rust that uses reference encodes lifetime information in it and the generated metadata for the crate includes this so that any crate depends on it can use that information to perform borrow checking.

        Originally posted by lowflyer View Post
        If I get that correctly, then rust needs the borrow checker to catch all the corner cases when it silently extends the lifetime of some objects to make the handling of references work. To me, it looks like rust somehow reverses the role of "reference" and "value". Or it "elevates" references to be a type of their own. It is an entirely different concept and the reason we don't understand each other when we use the words "reference" and "lifetime". Whether it is better or worse than e.g. C++ needs to be shown.
        In rust, reference is indeed treated as a first-class citizen.
        I don't think the role of reference and value is reversed, but reference does hold a special place in rust.

        Originally posted by lowflyer View Post
        When rustafarians (not you) say things like "the rust borrow-checker prevents all pitfalls that are in C++" I am reminded of cargo cults.
        Well, it certainly has limits: it can only prevent memory bugs (including the "multithread bug" I mentioned earlier) and sometimes it rejects some valid code and makes some other code (e.g. linker list, tree, graph) harder to implement.

        Hut it does prevent a lot of bugs for me.

        Comment


        • #34
          lowflyer Here is an example of running multiple async functions in one single thread while borrowing the same data: https://godbolt.org/z/Escf8hWT9
          The borrow checker ensures that you cannot invalid memory access here.

          Note that the async fn returns Future that contains the state and they are all allocated on the stack here.
          When running the future, it is just the same as calling a non-async function and they share the same stack, so if anything panics, it can be catched with catch_unwind.

          It can also use `Result` to return errors normally since it is just normal return value

          For C++, I just found out that using exceptions with coroutine in C++20 seems to a bit complicated due to stack, not sure whether it is feasible or not.

          This is an example of std::thread::scope that I took from the official doc but added more comment for you https://godbolt.org/z/Escf8hWT9
          The borrow checker ensures that you cannot have any use-after-free in the threads spawned.

          Comment


          • #35
            Originally posted by jacob View Post

            Congratulations! In your rush to produce "working code" you added copious amounts of boilerplate that are strictly irrelevant to the problem at hand, but by the same token you managed to completely miss the point of the exercise. What you wrote is "working" code that catches exceptions in the same thread that throws them. That mostly works (except for corner cases). The point is that you can't catch exceptions across threads: in this case, in the Rust version the main thread handles errors that occur in the worker threads. In C++, you can use exceptions, or you can spawn a thread, but in the latter case, you can't use exceptions to report errors to the caller.

            The C++ version similar to the Rust one would look something like this:

            Code:
            auto process_stuff(const std::vector<float> & value, int index) -> float
            {
            return value.at(index);
            }​
            [...]
            try {
            auto fut1 = std::async(process_stuff, vec, 7);
            auto fut2 = std::async(process_stuff, vec, 12);
            auto fut3 = std::async(process_stuff, vec, 3);
            print_result(fut1.get());
            print_result(fut2.get());
            print_result(fut3.get());
            }
            catch (std::exception& e) {
            fmt::print("Error: {}\n", e.what());
            }​
            This of course crashes.

            It's not even really equivalent to the Rust version because even if exceptions propagated across threads, it still wouldn't really work because to my best knowledge, you can't write a try/catch block in C++ that would catch one error all the while letting the remaining threads finish.

            To your credit, you implicitly acknowledged this by writing your tortuous imitation of the Result type to prove that it "can" be done in C++. There are of course many problems with that, among others the fact that with your solution you can only return strings containing error messages and not actual, structured errors (that would need more boilerplate), but it's an excellent example of the fanboyism I was deriding: you obviously don't care about engineering outcomes or code provability, you care about making a point about "your" language which seems to sit somewhere alongside your football club. For every problem in C++, anyone can of course devise a labyrinthine solution to work around it in one particular case. That doesn't constitute any merit though.
            Checkmate my friend! You show that you do not even understand rust.

            The code you provide *does not* represent what you think your rust snippet does. The code you provide *does not crash* as you claim. You fail to see that your hilarious rust example also does not "catch across threads". You fail to see that the "copious amounts of boilerplate" just prints. Nice printing causes "boilerplate" also in rust. You fail to see that std::string type can be replaced with std::exception. You fail to see that adding this in rust: ​​
            Code:
             ... -> Return<value, error>​​
            is equivalent to adding this in C++: ​​
            Code:
             try { ... } catch( error & ) { ... } ​​
            You fail to see that rust - and C++ - collect the failures *within* the thread while the handling is done outside. Both languages support the propagation of the error to the calling thread. In rust it is called "Result<>" while in C++ it is called "exception". I don't know for sure about rust, but I expect both languages let you move exceptions across threads.

            The following lines are a better equivalent to your C++ snippet above. Properly coded C++ does a very similar thing as properly coded rust. The code below will fail at the same point where the C++ example does. But I have my questions whether it can correctly print a result from a succeeding thread.
            ​​
            Code:
            async fn process_stuff(d: &input_data) -> isize {
            ...
            }
            
            fn do_the_processing() -> Result<results, Error> {
            let f1 = process_stuff(&data1);
            let f2 = process_stuff(&data2);
            let f3 = process_stuff(&data3);
            
            join!(f1, f2, f3);
            }

            I guess all your knowledge comes from what others have said and you're only here to bash C++. You should not do that. It would be better if you just shut up about C++ or rust. Lest I may continue to troll and point others to your debacle here.

            Comment


            • #36
              Originally posted by lowflyer View Post

              Checkmate my friend! You show that you do not even understand rust.

              The code you provide *does not* represent what you think your rust snippet does. The code you provide *does not crash* as you claim. You fail to see that your hilarious rust example also does not "catch across threads". You fail to see that the "copious amounts of boilerplate" just prints. Nice printing causes "boilerplate" also in rust. You fail to see that std::string type can be replaced with std::exception. You fail to see that adding this in rust: ​​
              Code:
               ... -> Return<value, error>​​
              is equivalent to adding this in C++: ​​
              Code:
               try { ... } catch( error & ) { ... } ​​
              You fail to see that rust - and C++ - collect the failures *within* the thread while the handling is done outside. Both languages support the propagation of the error to the calling thread. In rust it is called "Result<>" while in C++ it is called "exception". I don't know for sure about rust, but I expect both languages let you move exceptions across threads.

              The following lines are a better equivalent to your C++ snippet above. Properly coded C++ does a very similar thing as properly coded rust. The code below will fail at the same point where the C++ example does. But I have my questions whether it can correctly print a result from a succeeding thread.
              ​​
              Code:
              async fn process_stuff(d: &input_data) -> isize {
              ...
              }
              
              fn do_the_processing() -> Result<results, Error> {
              let f1 = process_stuff(&data1);
              let f2 = process_stuff(&data2);
              let f3 = process_stuff(&data3);
              
              join!(f1, f2, f3);
              }

              I guess all your knowledge comes from what others have said and you're only here to bash C++. You should not do that. It would be better if you just shut up about C++ or rust. Lest I may continue to troll and point others to your debacle here.
              Well we knew you hadn't a clue about computer science (since you thought that linear type system was some kind of meaningless buzzword) but I thought you might be at least a lowly cheap code pisser. Obviously you aren't even that. You don't know the first thing about C++. Your initial example was catching exceptions in the process_stuff function which, sorry to break it to you, means catching them in the same thread. You even took the pain to transform them manually into your result type because of course, catching them in the main thread absolutely WOULD crash (and it really does, just try it). In Rust on the other hand, contrary to what you believe, there is no exception raised at any point (Rust does have exceptions, but they are only used for irrecoverable errors). The process_stuff function doesn't contain any exception management, all errors are directly reported by creating instances of Result<> (including those from third party libraries etc). The Result<> types are in all cases returned through the same code path, error or not, and that means that they are a return value like any other, which can be easily passed across threads. But since you don't know Rust any more than you know C++, you obviously aren't capable of understanding that concept.

              Comment


              • #37
                Originally posted by jacob View Post
                <curses, badmouthing and warm air deleted />
                Apparently, you have neither the capacity nor the willingness to prove what you claim. You had your chance.​

                Comment

                Working...
                X