Help trying to transfer a structure from cranelift to rust

Hello everyone, I am doing a JIT with cranelift, and I am having problems transferring a structure from cranelift to rust. I want to know if the steps I follow are correct, I have this structure in rust:

#[repr(C)]
struct MyValue {
    pub r#type: i32,
    pub value: *const u8,
}

That's my basic structure. Later, from Cranelift, I call aligned_alloc to get aligned memory. Without moving from there, I set the value of the first field, and then by moving the necessary bytes for the second field, I insert the value of the second field there. So far, everything works fine; everything is correctly aligned and offset.

Using part of this example of how to call a function rust function from cranelift, I tried putting my struct as a parameter. So, this is how my function looks like:

pub extern "C" fn my_function(value: MyValue) {
    // ...
}

And then, what I do is call that function from cranelift and pass the pointer returned by aligned_alloc after inserting the fields. According to me, it should be valid to be passed to a Rust structure, but it's not. What happens is that only the first field is added correctly, that is, the r#type field is added correctly. The second field seems to have a memory address value, and every time I check the value of the second field, it changes to high numbers and sometimes negative values. That's why I think it has a memory address. Why is this happening? Is there a step that I might be missing when handling structures in Rust?

In summary, the only thing I do in memory is move 0 bytes from the initial pointer, and there I insert the first value. After that, I move the necessary bytes to insert the second field there. I don't do anything else; is there something I might be overlooking that could be preventing Rust from adding the second field correctly?

Here is my code, much of the code was taken from the link I put above on how to call a function in rust, and what was not it was taken from there I added it with comments, this is exactly what I do and here you can see what happens:

use std::mem;

use cranelift::{
    codegen::ir::{MemFlags, Type},
    prelude::{
        settings, AbiParam, Configurable, FunctionBuilder, FunctionBuilderContext, InstBuilder,
        Value,
    },
}; // ABI = Application Binary Interface

use cranelift_jit::{JITBuilder, JITModule}; // JIT = Just In Time

use cranelift_module::{Linkage, Module};

#[repr(C)]
#[derive(Debug)]
pub struct MyValue {
    pub r#type: i32,
    pub value: i64,
}

pub extern "C" fn my_function(value: MyValue) {
    println!("value = {:#?}", value);
}

fn main() {
    let mut jb: JITBuilder = {
        let mut flag_builder = settings::builder();
        flag_builder.set("use_colocated_libcalls", "false").unwrap();
        flag_builder.set("is_pic", "false").unwrap();
        /* isa = Instruction Set Architecture */
        let isa_builder = cranelift_native::builder().unwrap_or_else(|msg| {
            panic!("host machine is not supported: {}", msg);
        });
        let isa = isa_builder
            .finish(settings::Flags::new(flag_builder))
            .unwrap();
        JITBuilder::with_isa(isa, cranelift_module::default_libcall_names())
    };

    jb.symbol("my_function", my_function as *const u8);

    let mut module = JITModule::new(jb);
    let int = module.target_config().pointer_type();

    let mut ctx = module.make_context();
    let id = module
        .declare_function("test", Linkage::Export, &ctx.func.signature)
        .unwrap();

    {
        let mut fb_context = FunctionBuilderContext::new();
        let mut fb = FunctionBuilder::new(&mut ctx.func, &mut fb_context);
        let entry_block = fb.create_block();
        fb.switch_to_block(entry_block);
        fb.seal_block(entry_block);

        let mut sig = module.make_signature();
        sig.params.push(AbiParam::new(int));

        let callee = module
            .declare_function("my_function", Linkage::Import, &sig)
            .unwrap();

        // create MyValue
        let ptr = aligned_alloc(&mut fb, &mut module, int);

        // alloc the first field
        let r#type = fb.ins().iconst(int, 10);

        fb.ins().store(MemFlags::trusted(), r#type, ptr, 0);

        // alloc the second field
        let value = fb.ins().iconst(int, 20);

        fb.ins().store(MemFlags::trusted(), value, ptr, 8);

        // this should have MyValue
        let my_value = fb.ins().load(int, MemFlags::trusted(), ptr, 0);

        let local_callee = module.declare_func_in_func(callee, fb.func);

        fb.ins().call(local_callee, &[my_value]);

        fb.ins().return_(&[]);
        fb.finalize();
    }

    module.define_function(id, &mut ctx).unwrap();
    module.clear_context(&mut ctx);
    module.finalize_definitions().unwrap();
    let code: *const u8 = module.get_finalized_function(id);
    let code = unsafe { core::mem::transmute::<_, extern "C" fn()>(code) };
    code();
}

// calls aligned_alloc and returns the pointer to the allocated memory
// aligned_alloc is a C function that is defined in the C standard library
fn aligned_alloc(builder: &mut FunctionBuilder, module: &mut JITModule, int: Type) -> Value {
    let mut sig = module.make_signature();

    sig.params.push(AbiParam::new(int));
    sig.params.push(AbiParam::new(int));

    sig.returns.push(AbiParam::new(int));

    let callee = module
        .declare_function("aligned_alloc", Linkage::Import, &sig)
        .expect("problem declaring aligned_alloc function");

    let local_callee = module.declare_func_in_func(callee, builder.func);

    let alignment = builder.ins().iconst(int, mem::align_of::<MyValue>() as i64);
    let size = builder.ins().iconst(int, mem::size_of::<MyValue>() as i64);

    let call = builder.ins().call(local_callee, &[alignment, size]);

    builder.inst_results(call)[0]
}

This is just a guess, but are you sure the code generated by cranelift is using the extern "C" calling convention to call my_function()?

This sort of "I passed in X and Y, but some of the arguments are garbage on the other side" is usually because the function signatures or calling convention don't line up.

I'm not an expert on these topics, so I don't know exactly what you're referring to. But from what I understood, the function is called correctly, and only the first field of MyValue is assigned correctly, the other is not.

Cranelift only provides the primitives necessary to implement the calling convention. For struct types you need to handle the lowering to those primitives yourself. There are three common ways that a calling convention may lower structs:

  • Split it into one or more registers
  • Pass it by-reference (passing a pointer to the actual value).
  • Pass it at a fixed stack offset. In Cranelift this is ArgumentPurpose::StructArg(size_of_struct).

The easiest way is probably to explicitly pass your struct by-reference on the caller and callee side. This way you don't have to implement the full rules of the calling convention.

I do not know much about cranelift, but this code looks like you advertising my_function to take one "integer" parameter while it actually takes struct MyValue.

Apparently, cranelift just copies one "integer" worth of data to a function parameter, it somehow aliases with the r#type and the second field contains garbage. On top of this, advertisement code operates with some "ints", which i suppose are 64 bits, but fields in the struct have different bit-sizes.
I suggest to carefully think about what values should be in memory, how Rust sees them, and how you advertise memory layout to cranelift.
On a side note, it is fascinating that the only unsafe code in all of this is trasmuting whatever cranelift returns into a function.

How could I split it into one or more records? And how could I pass it by reference?

And I tried using ArgumentPurpose::StructArg(size_of_struct) and I get the same result, what could I be doing wrong?

I'm not very familiar with cranelift either, but the value I pass (int) is the value returned by cranelift_jit::backend::JITModule.target_config().pointer_type();, so I saw that cranelift does in the jit demo that They have it on github for all the parameters

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.