Stream: compiler development

Topic: roc-ray possible ABI issue?


view this post on Zulip Luke Boswell (Oct 18 2024 at 11:39):

Ok, I think I've got some kind of ABI problem.. but I'm not sure.

In the platform I have

# Effect.roc
HostColor : {
    # this is a hack to work around https://github.com/roc-lang/roc/issues/7142
    unused : I64,
    unused2 : I64,
    r : U8,
    g : U8,
    b : U8,
    a : U8,
}

setBackgroundColor : HostColor -> Task {} {}

This generates the following LLVM IR, which looks like I expect, basically passing a struct by value and geting a tag union back.

declare { [0 x i8], i8 } @roc_fx_setBackgroundColor({ i64, i64, i8, i8, i8, i8 })

In the host I'm representing the struct as follows

#[repr(C)]
pub struct HostColor {
    // this is a hack to work around https://github.com/roc-lang/roc/issues/7142
    pub unused: i64,
    pub unused2: i64,
    pub a: u8,
    pub b: u8,
    pub g: u8,
    pub r: u8,
}

#[no_mangle]
unsafe extern "C" fn roc_fx_setBackgroundColor(color: HostColor) -> RocResult<(), ()> {
     CLEAR_BACKGOUND.set(color);

    RocResult::ok(())
}

And for some reason... the information I'm getting in the host is not initialized. I thought it may be because I should actually be receiving a pointer &HostColor but, I'm pretty sure the LLVM IR is passing the struct by value. Anyway... messing with LLDB I can see the memory is not initialized.

* thread #1, name = 'main', stop reason = EXC_BAD_ACCESS (code=1, address=0x0)
    frame #0: 0x0000000100009084 roc-ray`roc_fx_setBackgroundColor(color=(unused = <parent is NULL>, unused2 = <parent is NULL>, a = <parent is NULL>, b = <parent is NULL>, g = <parent is NULL>, r = <parent is NULL>)) at main.rs:282:5
   279
   280  #[no_mangle]
   281  unsafe extern "C" fn roc_fx_setBackgroundColor(color: glue::HostColor) -> RocResult<(), ()> {
-> 282      CLEAR_BACKGOUND.set(color);
   283
   284      RocResult::ok(())
   285  }

view this post on Zulip Luke Boswell (Oct 18 2024 at 11:45):

From what I can tell from my investigation, it appears the issue may be in roc at the callsite, something related to the ABI.

view this post on Zulip Luke Boswell (Oct 18 2024 at 11:46):

It's only called in one place... that is in the fastcc wrapper

declare { [0 x i8], i8 } @roc_fx_setBackgroundColor({ i64, i64, i8, i8, i8, i8 })

define internal fastcc void @roc_fx_setBackgroundColor_fastcc_wrapper({ i64, i64, i8, i8, i8, i8 } %0, ptr %1) {
entry:
  %tmp = call { [0 x i8], i8 } @roc_fx_setBackgroundColor({ i64, i64, i8, i8, i8, i8 } %0), !dbg !222
  store { [0 x i8], i8 } %tmp, ptr %1, align 1, !dbg !222
  ret void, !dbg !222
}

view this post on Zulip Luke Boswell (Oct 18 2024 at 11:51):

I'm working on this branch https://github.com/lukewilliamboswell/roc-ray/tree/sprites

I was adding the feature for working with Textures.. but this is unrelated.... I got distracted by the set background color function not working as I expected it to.

view this post on Zulip Luke Boswell (Oct 18 2024 at 12:06):

I'm going to take a break. But I'm thinking I should check what rust is doing with that function and struct using godbolt next.

view this post on Zulip Brendan Hansknecht (Oct 18 2024 at 14:12):

Yeah looks like a roc bug....I'm actually quite a bit surprised by this one

view this post on Zulip Brendan Hansknecht (Oct 18 2024 at 14:12):

We should be passing a pointer to that strict due to how large it is

view this post on Zulip Brendan Hansknecht (Oct 18 2024 at 14:13):

I'm pretty sure rust is automatically converting it to a pointer

view this post on Zulip Brendan Hansknecht (Oct 18 2024 at 14:13):

This is something I would expect to just work....

view this post on Zulip Brendan Hansknecht (Oct 18 2024 at 14:13):

Rocs c abi could really use some love

view this post on Zulip Luke Boswell (Oct 18 2024 at 19:09):

Could it be specifically related to Task or hosted module? Does roc use a different ABI internally? Just looking for any pointers to narrow my search in the compiler... I'm thinking gen_llvm... but I suspect this might be back up in something earlier if it's Task related.

view this post on Zulip Dan G Knutson (Oct 18 2024 at 19:12):

is using RocList<u8> a reliable workaround for this c abi thing and 7142, or would that have the same problems?

view this post on Zulip Luke Boswell (Oct 18 2024 at 19:15):

I dont know. Haven't ran into this before with previous platforms. It seems strange. Using a RocList is probably a good workaround because its working elsewhere.

view this post on Zulip Luke Boswell (Oct 18 2024 at 19:16):

I've still been digging... I'll keep going for a little, but if it's over my head I'll look for a workaround and move on with the features.

view this post on Zulip Dan G Knutson (Oct 18 2024 at 19:23):

I was just wondering if I should change what I was doing

view this post on Zulip Luke Boswell (Oct 18 2024 at 19:24):

With what part?

view this post on Zulip Dan G Knutson (Oct 18 2024 at 19:28):

with inputs it seemed like just always passing a list of bytes was nice, so I was considering treating it like a default. if it also avoids this then that's good. I'm not sure if I'm inviting overhead this way (beyond the fat pointer I guess?), but it seems like a reasonable default

view this post on Zulip Luke Boswell (Oct 18 2024 at 19:32):

For adding features and avoiding these issues I think that is a great approach.

view this post on Zulip Luke Boswell (Oct 18 2024 at 19:52):

So back on this issue above... I wanted to know what rust does with something shaped like that. I built the following program.

fn main() {
    unsafe {
        roc_fx_setBackgroundColor(HostColor {
            a: 255,
            b: 255,
            g: 255,
            r: 255,
        })
    };
}

#[repr(C)]
pub struct HostColor {
    pub a: u8,
    pub b: u8,
    pub g: u8,
    pub r: u8,
}

#[no_mangle]
unsafe extern "C" fn roc_fx_setBackgroundColor(color: HostColor) {
    dbg!(color.a, color.b, color.g, color.r);
}

And then compiled it using $ RUSTFLAGS="--emit=llvm-ir" cargo build to get the LLVM IR.

I can see this,

; asdfasdf::main
; Function Attrs: uwtable
define internal void @_ZN8asdfasdf4main17h34e824c37946dda6E() unnamed_addr #1 !dbg !392 {
start:
  %_2 = alloca %HostColor, align 1
  store i8 -1, ptr %_2, align 1, !dbg !395
  %0 = getelementptr inbounds i8, ptr %_2, i64 1, !dbg !395
  store i8 -1, ptr %0, align 1, !dbg !395
  %1 = getelementptr inbounds i8, ptr %_2, i64 2, !dbg !395
  store i8 -1, ptr %1, align 1, !dbg !395
  %2 = getelementptr inbounds i8, ptr %_2, i64 3, !dbg !395
  store i8 -1, ptr %2, align 1, !dbg !395
  %3 = load i64, ptr %_2, align 1, !dbg !396
  call void @roc_fx_setBackgroundColor(i64 %3), !dbg !396
  ret void, !dbg !397
}

; Function Attrs: uwtable
define dso_local void @roc_fx_setBackgroundColor(i64 %0) unnamed_addr #1 !dbg !398 {
start:
  %f.dbg.spill.i28 = alloca ptr, align 8
  %x.dbg.spill.i29 = alloca ptr, align 8
  %_0.i30 = alloca %"core::fmt::rt::Argument<'_>", align 8
  %f.dbg.spill.i25 = alloca ptr, align 8
  %x.dbg.spill.i26 = alloca ptr, align 8
  %_0.i27 = alloca %"core::fmt::rt::Argument<'_>", align 8

Which looks to me like Rust is passing that by pointer.

view this post on Zulip Luke Boswell (Oct 18 2024 at 19:53):

I'm using LLVM instead of machine code here... because I assume that avoids any differences between x64 / aarch64 etc.

view this post on Zulip Brendan Hansknecht (Oct 18 2024 at 21:37):

That is not passing by pointer

view this post on Zulip Brendan Hansknecht (Oct 18 2024 at 21:37):

That is passing in a single register as an i64

view this post on Zulip Brendan Hansknecht (Oct 18 2024 at 21:37):

So it is packing the struct into one value

view this post on Zulip Luke Boswell (Oct 18 2024 at 22:54):

Random aside, do you know why rust represents the u8 as an i8 and assigns -1? Is there anything in that?

view this post on Zulip Luke Boswell (Oct 18 2024 at 23:58):

Just to clarify, this is a roc bug right?

Color : { r : U8, g : U8, b : U8, a : U8 }

setBackgroundColor : Color -> Task {} {}
; WHAT ROC GENERATES
declare { [0 x i8], i8 } @roc_fx_setBackgroundColor({ i8, i8, i8, i8 }) local_unnamed_addr

; WHAT RUST GENERATES
define dso_local void @roc_fx_setBackgroundColor(i64 %0) unnamed_addr #1 !dbg !398 {

ChatGPT Summary

In LLVM IR, `{ i8, i8, i8, i8 }` and `i64` are not the same, although they can represent the same amount of data (64 bits). The difference lies in how the data is structured and accessed.

### `{ i8, i8, i8, i8 }`

This represents a structure with four 8-bit integers. Each `i8` is an individual byte, and the structure explicitly defines the layout of these bytes. Accessing each byte would involve accessing the specific field within the structure.

### `i64`

This represents a single 64-bit integer. The entire 64 bits are treated as one unit, and accessing individual bytes would typically involve bitwise operations or shifts to isolate specific parts of the 64-bit value.

### Key Differences

1. **Data Layout**:
   - `{ i8, i8, i8, i8 }`: The data is laid out as four separate 8-bit fields.
   - `i64`: The data is a single 64-bit field.

2. **Access Patterns**:
   - `{ i8, i8, i8, i8 }`: Accessing individual bytes is straightforward as each byte is a separate field.
   - `i64`: Accessing individual bytes requires bitwise operations to extract the desired byte.

3. **Type Semantics**:
   - `{ i8, i8, i8, i8 }`: The type explicitly indicates a structure with four bytes.
   - `i64`: The type indicates a single 64-bit integer.

view this post on Zulip Brendan Hansknecht (Oct 19 2024 at 00:00):

This is the same issue as the tag c-abi thread

view this post on Zulip Brendan Hansknecht (Oct 19 2024 at 00:02):

Specifically this:

Struct and union parameters with sizes of two (eight in case of only SSE fields) pointers or fewer that are aligned on 64-bit boundaries are decomposed into "eightbytes" and each one is classified and passed as a separate parameter.[28]: 24  Otherwise they are replaced with a pointer when used as an argument.

view this post on Zulip Luke Boswell (Oct 19 2024 at 00:03):

Can you share the reference for this?

view this post on Zulip Luke Boswell (Oct 19 2024 at 00:03):

I'm going to try packing the bits into what Rust expects on the roc side. :smiley:

view this post on Zulip Brendan Hansknecht (Oct 19 2024 at 00:04):

That's just from the x86 call conv wikipedia page

view this post on Zulip Luke Boswell (Oct 19 2024 at 00:04):

Is it the same on aarch64?

view this post on Zulip Brendan Hansknecht (Oct 19 2024 at 00:10):

Not really, but in this case I think so:

If the argument is a Composite Type, and the size in double-words of the argument is no more than 8 minus NGRN, then the argument is copied into consecutive general-purpose registers, starting at x[NGRN]. The argument is passed as though it had been loaded into the registers from a double-word-aligned address, with an appropriate sequence of LDR instructions that load consecutive registers from memory. The contents of any unused parts of the registers are unspecified by this standard. The NGRN is incremented by the number of registers used. The argument has now been allocated.

view this post on Zulip Luke Boswell (Oct 19 2024 at 00:16):

I tried tricking roc to produce the rust equivalent using this helper. But I'm still not getting the right data out of roc. I can crash and see all 255's but using dbg in rust I'm getting

[src/main.rs:284:5] color = HostColor {
    a: 1,
    b: 0,
    g: 0,
    r: 0,
}
fromRGBA : { r : U8, g : U8, b : U8, a : U8 } -> I64
fromRGBA = \{ r, g, b, a } ->
    Num.shiftLeftBy 24 a
    |> Num.bitwiseOr (Num.shiftLeftBy 16 b)
    |> Num.bitwiseOr (Num.shiftLeftBy 8 g)
    |> Num.bitwiseOr (Num.shiftLeftBy 0 r)

I think I'm going to try make my own version and manually pack/unpack from an i64 next.

view this post on Zulip Luke Boswell (Oct 19 2024 at 00:16):

This is fun :smiley:

view this post on Zulip Luke Boswell (Oct 19 2024 at 00:35):

Hey, got it working :tada: :smiley:

view this post on Zulip Luke Boswell (Oct 19 2024 at 00:39):

fromRGBA : { r : U8, g : U8, b : U8, a : U8 } -> U64
fromRGBA = \{ r, g, b, a } ->
    (Num.intCast a |> Num.shiftLeftBy 24)
    |> Num.bitwiseOr (Num.intCast b |> Num.shiftLeftBy 16)
    |> Num.bitwiseOr (Num.intCast g |> Num.shiftLeftBy 8)
    |> Num.bitwiseOr (Num.intCast r)
pub struct RocColor(i64);

impl RocColor {
    pub fn from_rgba(r: u8, g: u8, b: u8, a: u8) -> RocColor {
        let color = ((a as i64) << 24) | ((b as i64) << 16) | ((g as i64) << 8) | (r as i64);
        RocColor(color)
    }

    pub fn to_rgba(self) -> (u8, u8, u8, u8) {
        let a = ((self.0 >> 24) & 0xFF) as u8;
        let b = ((self.0 >> 16) & 0xFF) as u8;
        let g = ((self.0 >> 8) & 0xFF) as u8;
        let r = (self.0 & 0xFF) as u8;
        (r, g, b, a)
    }
}

Added a bunch of tests, and it's also working nicely with the raylib stuff. :smiley:

view this post on Zulip Brendan Hansknecht (Oct 19 2024 at 00:56):

Yeah, manual packing is a decent workaround

view this post on Zulip Brendan Hansknecht (Oct 19 2024 at 00:57):

Also, I'm kinda stunned we didn't hit this before. Make me feel like there was a regression at somepoint

view this post on Zulip Brendan Hansknecht (Oct 19 2024 at 00:57):

Doesn't roc wasm4 use small types like this and not have problems?

view this post on Zulip Luke Boswell (Oct 19 2024 at 00:58):

I think we stuck to RocStr, and RocList for everything

view this post on Zulip Luke Boswell (Oct 19 2024 at 00:58):

I think this is the first time I've seen a record passed as an argument

view this post on Zulip Luke Boswell (Oct 19 2024 at 01:38):

I think it's the same with floats.

Vector2 : { x : F32, y : F32 }

setWindowSize : Vector2 -> Task {} {}
#[repr(C)]
pub struct RocVector2 {
    pub a: f32,
    pub b: f32,
}
; WHAT ROC GENERATES
@roc_fx_setWindowSize({ float, float })

; WHAT RUST GENERATES
@roc_fx_setWindowSize([2 x float] %0)

view this post on Zulip Brendan Hansknecht (Oct 19 2024 at 03:33):

In that case, I think roc and rust generate the same thing

view this post on Zulip Brendan Hansknecht (Oct 19 2024 at 03:33):

I don't think there should be a meaningful difference

view this post on Zulip Brendan Hansknecht (Oct 19 2024 at 03:33):

If there is, I'm not sure why/how. Both should pass in two float regs I think

view this post on Zulip Luke Boswell (Oct 19 2024 at 03:40):

I found a bit of a silly workaround... increases the size enough until it gets passed as a pointer.

RocRectangle := {
    x : F32,
    y : F32,
    width : F32,
    height : F32,
    unused : I64,
    unused2 : I64,
    unused3 : I64,
}

When we fix the bug we only need to change it in one part in the host and the platform.

view this post on Zulip Luke Boswell (Oct 19 2024 at 03:41):

My thoughts are that maybe... LLVM will be smart enough to optimise these unused fields out even.

view this post on Zulip Brendan Hansknecht (Oct 19 2024 at 04:30):

It wont


Last updated: Jul 06 2025 at 12:14 UTC