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 }
From what I can tell from my investigation, it appears the issue may be in roc at the callsite, something related to the ABI.
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
}
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.
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.
Yeah looks like a roc bug....I'm actually quite a bit surprised by this one
We should be passing a pointer to that strict due to how large it is
I'm pretty sure rust is automatically converting it to a pointer
This is something I would expect to just work....
Rocs c abi could really use some love
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.
is using RocList<u8> a reliable workaround for this c abi thing and 7142, or would that have the same problems?
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.
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.
I was just wondering if I should change what I was doing
With what part?
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
For adding features and avoiding these issues I think that is a great approach.
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.
I'm using LLVM instead of machine code here... because I assume that avoids any differences between x64 / aarch64 etc.
That is not passing by pointer
That is passing in a single register as an i64
So it is packing the struct into one value
Random aside, do you know why rust represents the u8 as an i8 and assigns -1? Is there anything in that?
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.
This is the same issue as the tag c-abi thread
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.
Can you share the reference for this?
I'm going to try packing the bits into what Rust expects on the roc side. :smiley:
That's just from the x86 call conv wikipedia page
Is it the same on aarch64?
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.
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.
This is fun :smiley:
Hey, got it working :tada: :smiley:
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:
Yeah, manual packing is a decent workaround
Also, I'm kinda stunned we didn't hit this before. Make me feel like there was a regression at somepoint
Doesn't roc wasm4 use small types like this and not have problems?
I think we stuck to RocStr, and RocList for everything
I think this is the first time I've seen a record passed as an argument
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)
In that case, I think roc and rust generate the same thing
I don't think there should be a meaningful difference
If there is, I'm not sure why/how. Both should pass in two float regs I think
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.
My thoughts are that maybe... LLVM will be smart enough to optimise these unused fields out even.
It wont
Last updated: Jul 06 2025 at 12:14 UTC