? operator, but for functions that don't return Result

I've recently started to understand the utility of the ? operator once you have good error mapping, and I'm using it quite a bit.

However, I have a 'C interface library' (written in Rust) that wraps my 'pure' Rust library (long story) and to make that outer API act 'C-like', I'm not using Result<> or Option<>. Instead, I have a

#[repr(C)]
pub enum Status {
   ErrorA, 
   ErrorB,
   ErrorC,
   Success
}

The issue I'm finding is that I can't use ? in the outer layer. I end up needing a pattern like this (in addition to the From that maps the proper error types):

impl From<Result<(), innercrate::Error>> for Status {
    fn from(res: Result<(), innercrate::Error>) -> Self {
        match res {
            Ok(_) => Status::Success,
            Err(e) => e.into(),
        }
    }
}

I realize what I'm trying to do (Result -> Enum) is probably niche enough that there isn't an 'idiomatic' solution, but does anyone have any bright ideas on what I can do to make this easy to read?

EDIT: In fact, the problem is not just the error mapping, because I need that to happen in addition to the magic control part of ?, namely that it return on Err and unwrap on success so that I can keep using the value.

In the future you will be able to implement the Try trait to your type and leverage the ? operator: Try in std::ops - Rust

8 Likes

? is shorthand, but it's not meant to be the all-purpose error handling operator. For cases where the behaviour of "unpackage Ok or propagate Err by return" is inappropriate, don't use it.

I'd use a match, and potentially package that match up inside of a helper or From impl:

impl From<innercrate::Error> for Status {
  fn from(err: innercrate::Error) -> Self {
    match err {
      /* … */
    }
  }
}

I would be inclined not to implement From for Result<T, innercrate::Error> personally, all else being equal, but it's not invalid or anything - just likely to be less flexible and harder to write.

1 Like

So what exactly is the problem with this piece of code? It's pretty obvious what it does, I'd even say it's idiomatic to wrap a C API using this approach. Do you have any concrete issues?

2 Likes

Nope, I was just going through older code I had written when first learning the language and noticing how useful ? has been in reducing boilerplate.

I wasn't sure if perhaps there was a way to do the same for the similar-but-not-the-same case I have with the C wrapper.

It's fine that there isn't; languages shouldn't add operator-level features for niche cases. I suspect there is some macro I could write for this, but I haven't tackled those yet. It also looks like Try might be a candidate in the future, as @moy2010 pointed out above.

I'm doing what you have above, in addition to the From<Result<(), innercrate:Error>> business. Truth be told, I can't really use the latter in many places, but the brilliance of the ? operator is that it hands me the Ok() value to use in whatever I have to do and takes care of the return otherwise.

Anyway, this is not a major problem, but I am at a stage in my Rust education where I keep finding very convenient idioms for problems I encounter, so I thought I'd go fishing for one here.

Yeah, I was going to comment that a macro would work if you liked the early-return semantics but cannot use the ? operator directly. It's a very simple macro (apologies for the contrived testbed): Rust Playground

If I understand your problem correctly, you have a Status C-like enum which is supposed to be returned from your extern functions, and there are lots of these functions, so you'd like to automate error conversion.

I would suggest defining a wrapper function which would just perform the conversion. Something like this:

fn call_with_status(f: impl FnOnce() -> Result<(), innercrate::Error>) -> Status {
    match f() {
        Ok(()) => Status::Success,
        Err(e) => Status::from(e), // or whatever other Result -> Status conversion logic you want
    }
}

Then you can use it inside your extern functions like this:

extern fn some_fn(a: A, b: B) -> Status {
    call_with_status(move || another_fn(a, b))
}

extern fn yet_another_fn(x: u32, y: Option<&mut Foo>) -> Status {
    call_with_status(move || {
        if x > 5 {
            return Err(innercrate::Error::LargeX);
        }
        if let Some(p) = y {
            *p = new_foo();
            Ok(())
        } else {
            Err(innercrate::Error::NullPtr)
        }
    })
}

This isn't quite right, but it did give me the confidence to write the macro I do want.

As I mentioned above, ? is brilliant because it will either return the unwrapped Ok() value for me to use or transfer control out of the function. I want something I can use inside the outer function (that forms the C API) as I translate from the Rust API, so I don't want to map final error codes, but have something that I can repeatedly use to extract "good" results or to bail with if things go wrong.

The C-like thing to do is (not always but commonly) to always return some Status enum and passing pointers for output. So I don't really want the macro/mapper to map anything to Status::Success; that's for the outer function to do when it thinks things have gone well.

This does what I want, I think.

The key thing is my modification of your wrapper, and the From function.

macro_rules! unwrap_or_return {
    ($expr:expr) => {
        match $expr {
            Ok(v) => v,
            Err(e) => {
                return e.into();
            }
        }
    };
}

This is close; see my reply above that uses a macro to return + map an error if we need to.

I really want something that acts like ? in that it will either hand me a the contents of an Ok() or bail from the function entirely.

what about a trait ?

trait Success {
     fn success(self) -> Status;
}

impl<T, E> Success for Result<T, E> {
    fn success(self) -> Result<Status,Status> {
         self.map(|_|Status::Success)
    }
}

fn test() -> Status {
    fn_that_return_result().success()?; // I don't like it that much
}

I think the problem is that what I titled this is not actually what I want. I didn't explain myself well. I'll edit the original post to clarify.

1 Like

I wasn't 100% certain what you were after, but yes, that looks reasonable. It's even closer to the try! macro than what I came up with ... which was in fact just based on try! in the first place. But also, you might want to look at the implementation in the standard library, because it deals with some hygiene details that unwrap_or_return! does not.

Yes, that's part of my rust macro education. I'll work on understanding it.

I'm excited to learn them, because they look very powerful, and I cut my teeth on the very powerful & useful Common Lisp macro system.

Unlike that system, which worried quite a bit about variable hygiene ((gensym)!), it looks like crate/import hygiene is the main concern here.

I gather that the $(,)? is there to gobble an optional trailing comma after the expr, but do you know why?

Is there a reason someone would write r#try(foo(),); ?

Yes, if I had to guess it's so you can split the invocation into multiple lines with a trailing comma.

I can't come up with a plausible example right now but imagine chains of Result-returning methods that you need to unwrap-or-return on. A trailing comma inside the macro token stream could help with syntactic consistency in some cases like this. But in general:

try!(
    do_something(),
),

Of course, try! is the ancestor of ?, and this example doesn't even compile any more (needs to be a raw identifier) but back in the day, this is the kind of code you might have seen. Similar to how ? is used today with long chains.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.