For context: I am trying to write a Rust wrapper over a C library.

Like many C libraries, most of its functions return an int. Positive return values are meaningful (provides information) and negative values are error codes.

To give an example, think of something like int get_items_from_record(const struct record *rec, struct item *items). A positive value indicates how many items were returned. -1 could mean ErrorA, -2 ErrorB, and so on.

Since this is Rust, I want to represent this kind of integer as Result<T, E>, e.g.:

enum LibError {
    A = -1,
    B = -2,
    // ....
}

// LibResult is ideally just represented as an integer.
type LibResult = Result<NonNegativeInteger, LibError>;

// Then I can pass LibResult values back to the C code as i32 trivially.

Is there a way/crate to do this?

  • Aloso
    link
    fedilink
    English
    arrow-up
    3
    arrow-down
    1
    ·
    edit-2
    1 year ago

    This is not possible, because Rust still stores a discriminant even when the enum values don’t overlap.

    As far as I can tell, the only situation where Rust doesn’t store a discriminant is when either the Ok or Err variant is zero-sized, and the other variant has a niche. So, Result&lt;(), ErrorEnum> can be represented as an integer, but Result can not.

    You can still use enums, and implement simple conversions like this:

    #[repr(i8)]
    pub enum Error {
        E1 = -1,
        E2 = -2,
        E3 = -3,
        E4 = -4,
    }
    
    #[repr(i8)]
    pub enum Success {
        S0 = 0,
        S1 = 1,
        S2 = 2,
        S3 = 3,
    }
    
    pub type LibResult = Result;
    
    pub fn number_to_result(value: i32) -> Option {
        match value {
            -4 ..= -1 => Some(Err(unsafe { std::mem::transmute(value as i8) })),
            0 ..= 3 => Some(Err(unsafe { std::mem::transmute(value as i8) })),
            _ => return None,
        }
    }
    
    pub fn result_to_number(res: LibResult) -> i32 {
        match res {
            Ok(value) => value as i32,
            Err(error) => error as i32,
        }
    }
    

    P.S. Sorry that the generics aren’t displayed due to Lemmy’s bad santiziation.

    • RunAwayFrog@sh.itjust.works
      link
      fedilink
      English
      arrow-up
      4
      arrow-down
      1
      ·
      edit-2
      1 year ago

      Discriminant is irrelevant and you’re not supposed to fuck with it.

      And there is zero reason to use unsafe/transmute for this.

      pub enum LibErr {
          ErrX,
          ErrY,
          ErrZ,
          Unknown(i32),
      }
      
      struct RetVal(i32);
      
      impl From for Result {
          fn from(RetVal(ret_val): RetVal) -> Self {
              if ret_val &lt; 0 {
                  match ret_val {
                      -1 => Err(LibErr::ErrX),
                      -2 => Err(LibErr::ErrY),
                      -3 => Err(LibErr::ErrZ),
                      unknown => Err(LibErr::Unknown(unknown)),
                  }
              } else { Ok(ret_val) }
          }
      }
      
      // RetVal(call_function()).into()
      
      • donnachaidh@lemmy.dcmrobertson.com
        link
        fedilink
        arrow-up
        3
        ·
        1 year ago

        And just to explicitly point out, your code’s also better because of the use of the standard traits. It took me a while to get into the habit, but using what’s already there is always a good idea.

      • Aloso
        link
        fedilink
        arrow-up
        2
        ·
        1 year ago

        Discriminant is irrelevant and you’re not supposed to fuck with it

        It matters because the conversion between i32 and the Result is only “free” if they have the same layout (which they do not, because of the discriminant). So a more costly conversion method is required.

        And there is zero reason to use unsafe/transmute for this.

        You are right, because the compiler is able to optimize your code quite well. However, if that optimization were to break at some point (as there is no guarantee that an optimization will continue to work in the future), it would become less efficient.

        • donnachaidh@lemmy.dcmrobertson.com
          link
          fedilink
          arrow-up
          5
          ·
          1 year ago

          That seems like strong premature optimisation. Perhaps worth a note, but I’d presume the majority of people the majority of the time wouldn’t need to worry about that.

          • Barbacamanitu@lemmy.world
            link
            fedilink
            arrow-up
            2
            arrow-down
            1
            ·
            1 year ago

            For real. Unless he’s converting between results and ints millions of times a second, I think he’s going to be just fine using the idiomatic solution. That transmute shit I’d wack lol

        • Barbacamanitu@lemmy.world
          link
          fedilink
          arrow-up
          1
          arrow-down
          1
          ·
          1 year ago

          So what! Who cares if it’s free? Write first, profile and optimize later. Not everyone cares about whether the conversion is free. Simply matching and converting to the right integer is fast enough.

          • orangeboats@lemmy.worldOP
            link
            fedilink
            arrow-up
            1
            ·
            edit-2
            1 year ago

            The reason I asked the question, was that I wanted to keep an int an int throughout the program.

            It’s not for performance reasons, it’s just that I feel like there is a certain elegance in keeping things type safe entirely “for free” much like how Option&lt;&amp;T> is actually just a regular T*, even if it could be pointless in the big picture.

            • Barbacamanitu@lemmy.world
              link
              fedilink
              arrow-up
              1
              ·
              1 year ago

              Well if you want to keep an int as an int, then use an int. If you want to use Results, use results. Like I said to the other commenter: unless this code is in a very tight loop where performance is crucial, you’ll be fine just implementing From/Into to change your Result to and from an int. You’re already crossing a language boundary by calling c code from Rust. Is it that much more of an issue to just convert the types when you need to?

              Trying to store the results as an int in some magical way just screams premature optimization. You have abstraction tools at your disposal. They may not be zero cost, but they are cheap. Use them.

              • orangeboats@lemmy.worldOP
                link
                fedilink
                arrow-up
                1
                ·
                1 year ago

                First and foremost: It’s not about optimization, as I have mentioned before. Never once have I intended to optimize the conversion, because I know it is pointless. Stop making that assumption and only then we can continue the discussion.

                There is no reason why people cannot use Rust as “C, but actually type-safe”. A type-safe representation of C’s error code pattern is a part of that. This way the code is “backwards compatible” with debuggers designed for C/C++, such that “-EINVAL” is actually displayed as “-EINVAL” in the debugger and not a mysterious struct of (discriminant, data).

                • Barbacamanitu@lemmy.world
                  link
                  fedilink
                  arrow-up
                  1
                  ·
                  1 year ago

                  Oh I see. I misunderstood the reason for wanting it represented as an int.

                  I’m wondering if you could just create a wrapper type that only has an int as a member, but then implement a trait on it so that it can act like a result. That, or just pass around your int type in the rust code, and when you need it to act like a result you do a conversion from int to result. Your debugger wouldn’t show it as an int at that point, but it wouldn’t show any other Result as an int anyway so it would br consistent with other rust code. If this still doesn’t work, you could even make a struct that contains both the int and the result and keeps then synchronized. Then, when debugging, you could look into that struct and see the int value like you want.