matsadler / magnus

Ruby bindings for Rust. Write Ruby extension gems in Rust, or call Ruby from Rust.

Home Page:https://docs.rs/magnus/latest/magnus/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Error compiling magnus v0.5.1 on arm-linux

yammine opened this issue Β· comments

Hello πŸ‘‹
First off, thank you for this project it's great.

I've noticed a compilation error on arm-linux after updating from version 0.4 to 0.5

The error:

error[E0308]: mismatched types
   --> /home/runner/work/roaring-rb/roaring-rb/tmp/cargo-vendor/magnus/src/r_bignum.rs:295:13
    |
291 |     fn sign(self) -> u64 {
    |                      --- expected `u64` because of return type
...
295 |             r_basic.as_ref().flags & (ruby_fl_type::RUBY_FL_USER1 as VALUE)
    |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `u64`, found `u32`
    |
help: you can convert a `u32` to a `u64`
    |
295 |             (r_basic.as_ref().flags & (ruby_fl_type::RUBY_FL_USER1 as VALUE)).into()
    |             +                                                               ++++++++

Here is a link to the GH Action run where I've observed this: link
All other platforms successfully compile magnus so I think it's isolated to the arm-linux platform.

If someone knows off the top of their head how to fix this I'd love to meaningfully contribute by submitting a patch.

Thanks for your attention

Hey, thanks for the bug report.

So the issue here is arm-linux is a 32 bit platform (aarch64-linux is 64 bit arm), and when I wrote this code I wasn't thinking about 32 bit platforms (I'm happy to get Magnus compiling for 32 bit platforms, but no guarantee it'll actually do what it's meant to do).

I'll explain what's going on from the ground up so that you have the context if you'd like to put together a patch. Sorry if any of this is stuff you know already.

In Ruby's internals / the Ruby C API all Ruby objects are represented by VALUE, this is effectively a pointer to the object (all of Magnus's types are just wrappers around a VALUE).

Ruby defines VALUE as an integer the size of a pointer, which is basically Rust's usize. But something gets lost in translation so it ends up in Rust as VALUE being defined as an alias to u64 on 64 bit platforms, and an alias to u32 on 32 bit platforms.

So VALUE is a pointer to a Ruby object, and r_basic is the thing it points to. r_basic usually holds some very basic information about the object and then further pointers to specifics. One of the basic bits of data is flags this is a bitfield that encodes some information about the object. In the case of RBignum one of the things in the flags is if the bignum is positive or negative. For some weird historical reason Ruby defines the type of flags as VALUE, even though it's not a Ruby object (relying on knowing that under the hood VALUE is just an integer).

To check the bit in flags that represents whether the bignum is positive or negative we & (bitwise and) the flags with the constant ruby_fl_type::RUBY_FL_USER1. The type of this isn't VALUE, but Rust's & operator wants the types on both sides to be the same, so we cast it to VALUE. As both sides of the & are a VALUE, the end result is VALUE. As the result is most definitely not a Ruby object I didn't want to use VALUE as the return type, so used u64.

This works because on all the platforms I test on VALUE is an alias of u64, but it breaks on arm-linux because there VALUE is an alias of u32, which then won't match the return type.

One other detail, sign/is_positive/is_negative exist in Ruby's C API as macros, which can't be used from Rust, so have to be reimplemented for Rust. The sign function isn't public, it's only here to implement is_positive. While sign does exist in Ruby's C API I didn't have any intention of exposing it publicly as it's just kind of weird, like it's called sign and the return value is 0 for negative or any other number for positive, how does that make sense? So sign doesn't need to exist, can have a different return type, whatever, along as is_positive stays working the same.

I can think of a few different ways to fix this, I don't have any strong reason to prefer one over another, and you might be able to think of another. Hopefully that's enough to go off without just telling you exactly what to do (which seems like it'd be boring for you).

Thank you for the very thoughtful reply! :)
I didn't know any of this, so it was very educational for me.

I actually would appreciate some pointers on where I might begin to implement support for 32bit platforms, though no promises on my ability to actually get it done lol

I'll just drop support for that platform for my gem in the meantime

Sorry, this is a bit long winded, I'm trying to be quick and weirdly that means writing more because I'm not spending the time to edit it down.

The body of the sign function works just fine on 64 bit or 32 bit, but the result doesn't match up with the return type declared in the function signature. This is because the function body is evaluating to VALUE, which only sometimes matches u64.

One way to fix this is to set the return value in the function signature to VALUE, that way it'll always match. The one place it's used is is_positive where it's compared with 0. Rust infers the type of integer literals, so as it is being compared against the result of sign the 0 is automatically the same type as whatever sign returns.
I don't really like this approach as while VALUE is technically just an alias of u64/u32 it is semantically a Ruby object, which the return of sign is not.

Another way would be the suggestion given in the error message, to tack on an .into(). This would work. On 32 bit platforms it becomes a conversion from u32 to u64, which is fine as u32 fits in u64, and on 64 bit platforms it's a conversion from u64 to u64, which obviously works. The problem is it so obviously works that it's pointless, and cargo clippy warns us with useless conversion to the same type .

A third possibility would be to use usize as the return value. usize is a distinct type, not an alias like VALUE, but has similar properties to VALUE in that it's the same size as u64 on 64 bit platforms and the same size as u32 on 32 bit platforms. This can't be done with .into() because the Rust compiler knows that sometimes usize is only 32 bits, but it doesn't know that in that case VALUE will always alias to u32, so it errors when VALUE is u64 because u64 isn't guaranteed to fit into usize. This can be done with an as cast though, because as is kind of lazy and doesn't care about that (some people are opposed to using as because of this. I'm fine with as in this code). Note that with the & the default precedence of a & b as usize would be a & (b as usize), so you have to wrap the whole expression in parens, like (a & b) as usize.

And the final option I can think of is, sign is only used in one place. I don't plan on using it anywhere else, or making it public, so the body of sign could just be inlined into is_positive. As we're never declaring a conflicting return type, and the type of the 0 it's compared against is inferred, it'll work regardless of what VALUE is an alias to.

Hey, sorry if you've not had time to contribute. I went ahead and fixed this in 7f42447 and put out a 0.5.2 release. I went with the 'inline sign to is_positive approach.