I ran into this issue at work today, and haven't figured out a workaround; can anyone think of an idea for a workaround?
can you dump the llvm ir for both rust and the roc program? it's likely an incompatibility there
roc_fx_envVar
on the roc side:
declare void @roc_fx_envVar(%str.RocStr* sret(%str.RocStr), { i8*, i64, i64 }*)
on the rust side:
; Function Attrs: uwtable
define void @roc_fx_envVar(ptr sret(%"roc_std::roc_str::RocStr") %0, ptr align 8 %1) unnamed_addr #1 !dbg !10953 {
start:
%_18 = alloca [1 x { ptr, ptr }], align 8
%_11 = alloca %"core::fmt::Arguments<'_>", align 8
%_3 = alloca %"core::fmt::Arguments<'_>", align 8
%roc_str = alloca ptr, align 8
store ptr %1, ptr %roc_str, align 8
call void @llvm.dbg.declare(metadata ptr %roc_str, metadata !10957, metadata !DIExpression()), !dbg !10958
; call core::fmt::Arguments::new_v1
call void @_ZN4core3fmt9Arguments6new_v117hc9cd7c67f24d5224E(ptr sret(%"core::fmt::Arguments<'_>") %_3, ptr align 8 @all
oc1112, i64 1, ptr align 8 @alloc1114, i64 0), !dbg !10959
; call std::io::stdio::_print
call void @_ZN3std2io5stdio6_print17h8769d5b8d07b47a7E(ptr %_3), !dbg !10959
; call core::fmt::ArgumentV1::new_debug
%2 = call { ptr, ptr } @_ZN4core3fmt10ArgumentV19new_debug17h70af07452f17459eE(ptr align 8 %roc_str), !dbg !10960
%_19.0 = extractvalue { ptr, ptr } %2, 0, !dbg !10960
%_19.1 = extractvalue { ptr, ptr } %2, 1, !dbg !10960
%3 = getelementptr inbounds [1 x { ptr, ptr }], ptr %_18, i64 0, i64 0, !dbg !10960
%4 = getelementptr inbounds { ptr, ptr }, ptr %3, i32 0, i32 0, !dbg !10960
store ptr %_19.0, ptr %4, align 8, !dbg !10960
%5 = getelementptr inbounds { ptr, ptr }, ptr %3, i32 0, i32 1, !dbg !10960
store ptr %_19.1, ptr %5, align 8, !dbg !10960
; call core::fmt::Arguments::new_v1
call void @_ZN4core3fmt9Arguments6new_v117hc9cd7c67f24d5224E(ptr sret(%"core::fmt::Arguments<'_>") %_11, ptr align 8 @alloc1117, i64 2, ptr align 8 %_18, i64 1), !dbg !10960
; call std::io::stdio::_print
call void @_ZN3std2io5stdio6_print17h8769d5b8d07b47a7E(ptr %_11), !dbg !10960
; call <roc_std::roc_str::RocStr as core::default::Default>::default
call void @"_ZN67_$LT$roc_std..roc_str..RocStr$u20$as$u20$core..default..Default$GT$7default17h4cb6e5af3316de95E"(ptr sret(%"roc_std::roc_str::RocStr") %0), !dbg !10961
ret void, !dbg !10962
}
so they're both void and have the same name, so the differences look like:
first argument: (pointer to returned RocStr)
Rust: ptr sret(%"roc_std::roc_str::RocStr") %0
Roc: %str.RocStr* sret(%str.RocStr)
second argument: (RocStr passed by reference)
Rust: ptr align 8 %1
Roc: { i8*, i64, i64 }*
aside from the notation differences I think those are saying the same thing, so doesn't look like a type disagreement unless I'm missing something
and it ends in what certainly appears to be calling RocStr::default()
, writing it into the sret
pointer (so, "returning" it), and then actually returning void
; call <roc_std::roc_str::RocStr as core::default::Default>::default
call void @"_ZN67_$LT$roc_std..roc_str..RocStr$u20$as$u20$core..default..Default$GT$7default17h4cb6e5af3316de95E"(ptr sret(%"roc_std::roc_str::RocStr") %0), !dbg !10961
ret void, !dbg !10962
hm, this might be something:
define internal fastcc void @roc_fx_envVar_fastcc_wrapper(%str.RocStr* %arg, %str.RocStr* %arg1) {
entry:
%to_cc_type_ptr = bitcast %str.RocStr* %arg to { i8*, i64, i64 }*
call void @roc_fx_envVar(%str.RocStr* %arg1, { i8*, i64, i64 }* %to_cc_type_ptr)
ret void
}
the fastcc wrapper reverses the argument order; it takes the return pointer last rather than first
but then again, the fastcc wrapper is only called here:
define internal fastcc void @Effect_effect_closure_envVar_669c1355a3e727bb53dd458f2e96e48571aa45dfabcfb4b7de1689484f11({}
%"13", %str.RocStr* %closure_arg_envVar_0, %str.RocStr* %arg) {
entry:
%result_value = alloca %str.RocStr, align 8
call fastcc void @roc_fx_envVar_fastcc_wrapper(%str.RocStr* %closure_arg_envVar_0, %str.RocStr* nonnull %result_value)
call fastcc void @"#Attr_#dec_1"(%str.RocStr* %closure_arg_envVar_0)
%i = bitcast %str.RocStr* %arg to i8*
%i1 = bitcast %str.RocStr* %result_value to i8*
call void @llvm.memcpy.p0i8.p0i8.i64(i8* noundef nonnull align 8 dereferenceable(24) %i, i8* noundef nonnull align 8 der
eferenceable(24) %i1, i64 24, i1 false)
ret void
}
which looks to be correctly passing the return pointer last
(the full dumps, for reference:)
yeah our fastcc puts return-by-pointer arguments last. But C ABI wants in the first position
what is the alignment of RocStr, { i8*, i64, i64 }*
?
depends on your platform, right?
but I'd say 8 because of the i64
I'd assume 8 because it's a pointer and this is a 64-bit system
noice, after some dialogue with chatGPT, found a fix! The Rust host code was wrong; this is the corrected implementation:
#[no_mangle]
pub extern "C" fn rust_main() {
let size = unsafe { roc_main_size() } as usize;
let layout = Layout::array::<u8>(size).unwrap();
unsafe {
// TODO allocate on the stack if it's under a certain size
let buffer = std::alloc::alloc(layout);
roc_main(buffer);
let closure_output = call_the_closure(buffer);
let answer: *mut RocStr = closure_output.cast();
println!("Answer was: {:?}", &*answer);
std::alloc::dealloc(buffer, layout);
std::alloc::dealloc(closure_output, layout);
}
}
unsafe fn call_the_closure(closure_data_ptr: *const u8) -> *mut u8 {
// return type changed
let size = size_Fx_result() as usize;
let layout = Layout::array::<u8>(size).unwrap();
let buffer = std::alloc::alloc(layout) as *mut u8;
call_Fx(
// This flags pointer will never get dereferenced
MaybeUninit::uninit().as_ptr(),
closure_data_ptr as *const u8,
buffer as *mut u8,
);
// Don't deallocate the buffer here
// std::alloc::dealloc(buffer, layout);
buffer // return the buffer
}
so I need to update glue accordingly
for my own future reference, the glue we want to generate:
pub extern "C" fn rust_main() {
let answer = mainForHost().force_thunk();
println!("Answer was: {:?}", unsafe { answer });
}
#[repr(C)]
#[derive(Debug, Clone)]
pub struct RocFunction_71 {
closure_data: roc_std::RocList<u8>,
}
impl RocFunction_71 {
pub fn force_thunk(self) -> roc_std::RocStr {
extern "C" {
fn roc__mainForHost_0_caller(
arg0: *const (),
closure_data: *mut u8,
output: *mut roc_std::RocStr,
);
fn roc__mainForHost_0_result_size() -> i64;
}
let mut output = MaybeUninit::uninit();
let closure_ptr =
(&mut core::mem::ManuallyDrop::new(self.closure_data)) as *mut _ as *mut u8;
unsafe {
roc__mainForHost_0_caller(&(), closure_ptr, output.as_mut_ptr());
output.assume_init()
}
}
}
pub fn mainForHost() -> RocFunction_71 {
extern "C" {
fn roc__mainForHost_1_exposed_generic(_: *mut RocFunction_71);
}
let mut ret = core::mem::MaybeUninit::uninit();
unsafe {
roc__mainForHost_1_exposed_generic(ret.as_mut_ptr());
ret.assume_init()
}
}
Last updated: Jul 06 2025 at 12:14 UTC