Calling a Rust function from Cranelift

I have been experimenting with Cranelift, This is where I have got to. Mostly I think I understand what is going on. It seems to work as expected. The "scary" bit is taking the raw address of rustfunc and then having to declare the parameters and return type to match correctly. It seems there is no way in Rust to automate this (unless I am missing something). I was expecting to have to choose and declare an explicit calling convention, but it seems to work correctly without doing that. The other slight mystery is the settings, "use_colocated_libcalls" and "is_pic". I have no idea what "is_pic" means, or where it is documented. I copied these (and other bits of code) from GitHub - bytecodealliance/cranelift-jit-demo: JIT compiler and runtime for a toy language, using Cranelift

Edit: in view of the answer, I edited the code inserting extern "C" for the declaration of rustfunc, also on the declaration of the Cranelift function ( the transmute line ).

use cranelift::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};

extern "C" fn rustfunc(x: isize, y: isize) -> isize {
    println!("Hello from rustfunc x={} y={}", x, y);
    x - y
}

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("rustfunc", rustfunc as *const u8);

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

    let mut ctx = module.make_context();
    ctx.func.signature.params.push(AbiParam::new(int));
    ctx.func.signature.returns.push(AbiParam::new(int));
    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.append_block_params_for_function_params(entry_block);
        fb.switch_to_block(entry_block);
        fb.seal_block(entry_block);

        let x: Value = fb.block_params(entry_block)[0];

        // rustfunc(x+60,60)
        let result: 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("rustfunc", Linkage::Import, &sig)
                .unwrap();
            let local_callee = module.declare_func_in_func(callee, fb.func);
            let mut arg_values = Vec::new();
            let sixty: Value = fb.ins().iconst(int, 60);
            let val = fb.ins().iadd(x, sixty);
            arg_values.push(val);
            arg_values.push(sixty);
            let call = fb.ins().call(local_callee, &arg_values);
            fb.inst_results(call)[0]
        };
        fb.ins().return_(&[result]);
        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(isize) -> isize>(code) };
    let x = 40;
    let z = code(x);
    println!("code({})={}", x, z);
    assert_eq!(x, z);
}

Note that unless the rust function is extern "C", there's probably no sound way to call it from cranelift since there's no guarantee what the ABI is.

1 Like

That's what I thought. Nevertheless it seems to work ( I know this is not proof it is correct!).

I did print the (default) calling convention out and it is windows_fastcall.

The thing is, if the Rust function is declared as extern "C" what Cranelift callconv option should be used?

I don't see it here.

[ I did google "pic" and it stands for "position independent code" ( I think this means code that can be copied byte-for-byte to any memory location and it will still work - e.g. jumps are relative ). ]

Edit: I just tried declaring the function as extern "C" and the program still works.
Perhaps extern "C" on windows actually has no effect at all?

You can do this with a bit of trait magic.

I'm on my phone at the moment so my example won't compile, but it'll go something like this:


enum Type {
  // Whatever makes sense for cranelift
} 

trait AbiType {
  fn get_type() -> Type:
} 

trait Func {
  fn signature() -> FunctionSignature;
}

impl<Ret: AbiType> Func for unsafe extern "C" fn() {
  fn signature() -> FunctionSignature {
    let mut builder= FunctionSignature builder::new();
    builder.set_return_type(<Ret as AbiType>::get_type());
  builder.finish()
  }
}

impl<Arg1: AbiType, Ret: AbiType> Func for unsafe extern "C" fn() {
  fn signature() -> FunctionSignature {
    let mut builder= FunctionSignature builder::new();
 
    builder.push_argument(<Arg1 as AbiType>::get_type());

    builder.set_return_type(<Ret as AbiType>::get_type());

  builder.finish()
  }
}

You should be able to implement Func for functions with 1, 2, 3, etc. arguments using a declarative macro.

Pretty much, except when you say "code", the thing being moved around in memory is the executable's image, rather than individual functions.

That means the binary does things like using a PLT instead of hard-coding the addresses for library functions and other external things that might be loaded at runtime.

Most operating systems will implement Address Space Layout Randomisation so your executable (and any dynamic libraries it uses) can be loaded into memory at random locations. That means an attacker can't use static analysis to, for example, determine the address of something like system() from libc and inject some specially crafted shell code which does naughty things.

If your binary is compiled with Position Independent Code it means (among other things) ASLR can do it's thing, otherwise the OS has to disable it.

2 Likes

target_isa.default_call_conv() and CallConv::triple_default(triple) both return the right CallConvforextern "C"functions on the respective target. For example on x86_64 that would beCallConv::SystemV, except for windows where it is CallConv::WindowsFastcall`.

1 Like

Ah, that explains everything, thank you! Is this documented anywhere? If not, I think it should be.

I don't think this is documented anywhere. In general a lot of things in Cranelift are not as well documented as they should be.

1 Like