@ -1,96 +1,392 @@
use core ::mem ;
//! Provides functionality for compiling and running CLIF IR for `run` tests.
use core ::{ mem , ptr } ;
use cranelift_codegen ::binemit ::{ NullRelocSink , NullStackmapSink , NullTrapSink } ;
use cranelift_codegen ::ir ::Function ;
use cranelift_codegen ::ir ::{ condcodes ::IntCC , Function , InstBuilder , Signature , Type } ;
use cranelift_codegen ::isa ::TargetIsa ;
use cranelift_codegen ::{ settings , Context } ;
use cranelift_codegen ::{ ir , settings , CodegenError , Context } ;
use cranelift_frontend ::{ FunctionBuilder , FunctionBuilderContext } ;
use cranelift_native ::builder as host_isa_builder ;
use memmap ::MmapMut ;
use cranelift_reader ::DataValue ;
use log ::trace ;
use memmap ::{ Mmap , MmapMut } ;
use std ::cmp ::max ;
use std ::collections ::HashMap ;
use thiserror ::Error ;
/// Run a function on a host
pub struct FunctionRunner {
function : Function ,
/// Compile a single function.
///
/// Several Cranelift functions need the ability to run Cranelift IR (e.g. `test_run`); this
/// [SingleFunctionCompiler] provides a way for compiling Cranelift [Function]s to
/// [CompiledFunction]s and subsequently calling them through the use of a [Trampoline]. As its
/// name indicates, this compiler is limited: any functionality that requires knowledge of things
/// outside the [Function] will likely not work (e.g. global values, calls). For an example of this
/// "outside-of-function" functionality, see `cranelift_simplejit::backend::SimpleJITBackend`.
///
/// ```
/// use cranelift_filetests::SingleFunctionCompiler;
/// use cranelift_reader::parse_functions;
///
/// let code = "test run \n function %add(i32, i32) -> i32 { block0(v0:i32, v1:i32): v2 = iadd v0, v1 return v2 }".into();
/// let func = parse_functions(code).unwrap().into_iter().nth(0).unwrap();
/// let mut compiler = SingleFunctionCompiler::with_default_host_isa();
/// let compiled_func = compiler.compile(func).unwrap();
/// println!("Address of compiled function: {:p}", compiled_func.as_ptr());
/// ```
pub struct SingleFunctionCompiler {
isa : Box < dyn TargetIsa > ,
trampolines : HashMap < Signature , Trampoline > ,
}
impl FunctionRunner {
/// Build a function runner from a function and the ISA to run on (must be the host machine's ISA)
pub fn new ( function : Function , isa : Box < dyn TargetIsa > ) -> Self {
Self { function , isa }
impl SingleFunctionCompiler {
/// Build a [SingleFunctionCompiler] from a [TargetIsa]. For functions to be runnable on the
/// host machine, this [TargetISA] must match the host machine's ISA (see [with_host_isa]).
pub fn new ( isa : Box < dyn TargetIsa > ) -> Self {
let trampolines = HashMap ::new ( ) ;
Self { isa , trampolines }
}
/// Build a function runner using the host machine's ISA and the passed flags
pub fn with_host_isa ( function : Function , flags : settings ::Flags ) -> Self {
/// Build a [SingleFunctionCompiler] using the host machine's ISA and the passed flags.
pub fn with_host_isa ( flags : settings ::Flags ) -> Self {
let builder = host_isa_builder ( ) . expect ( "Unable to build a TargetIsa for the current host" ) ;
let isa = builder . finish ( flags ) ;
Self ::new ( function , isa )
Self ::new ( isa )
}
/// Build a function runner using the host machine's ISA and the default flags for this ISA
pub fn with_default_host_isa ( function : Function ) -> Self {
/// Build a [SingleFunctionCompiler] using the host machine's ISA and the default flags for this
/// ISA.
pub fn with_default_host_isa ( ) -> Self {
let flags = settings ::Flags ::new ( settings ::builder ( ) ) ;
Self ::with_host_isa ( function , flags )
}
/// Compile and execute a single function, expecting a boolean to be returned; a 'true' value is
/// interpreted as a successful test execution and mapped to Ok whereas a 'false' value is
/// interpreted as a failed test and mapped to Err.
pub fn run ( & self ) -> Result < ( ) , String > {
let func = self . function . clone ( ) ;
if ! ( func . signature . params . is_empty ( )
& & func . signature . returns . len ( ) = = 1
& & func . signature . returns . first ( ) . unwrap ( ) . value_type . is_bool ( ) )
{
return Err ( String ::from (
"Functions must have a signature like: () -> boolean" ,
) ) ;
Self ::with_host_isa ( flags )
}
if func . signature . call_conv ! = self . isa . default_call_conv ( ) {
return Err ( String ::from (
"Functions only run on the host's default calling convention; remove the specified calling convention in the function signature to use the host's default." ,
) ) ;
/// Compile the passed [Function] to a [CompiledFunction]. This function will:
/// - check that the default ISA calling convention is used (to ensure it can be called)
/// - compile the [Function]
/// - compile a [Trampoline] for the [Function]'s signature (or used a cached [Trampoline];
/// this makes it possible to call functions when the signature is not known until runtime.
pub fn compile ( & mut self , function : Function ) -> Result < CompiledFunction , CompilationError > {
let signature = function . signature . clone ( ) ;
if signature . call_conv ! = self . isa . default_call_conv ( ) {
return Err ( CompilationError ::InvalidTargetIsa ) ;
}
// Compile the function itself.
let code_page = compile ( function , self . isa . as_ref ( ) ) ? ;
// Compile the trampoline to call it, if necessary (it may be cached).
let isa = self . isa . as_ref ( ) ;
let trampoline = self
. trampolines
. entry ( signature . clone ( ) )
. or_insert_with ( | | {
let ir = make_trampoline ( & signature , isa ) ;
let code = compile ( ir , isa ) . expect ( "failed to compile trampoline" ) ;
Trampoline ::new ( code )
} ) ;
Ok ( CompiledFunction ::new ( code_page , signature , trampoline ) )
}
}
#[ derive(Error, Debug) ]
pub enum CompilationError {
#[ error( " Cross-compilation not currently supported; use the host's default calling convention \
or remove the specified calling convention in the function signature to use the host 's default . " ) ]
InvalidTargetIsa ,
#[ error( " Cranelift codegen error " ) ]
CodegenError ( #[ from ] CodegenError ) ,
#[ error( " Memory mapping error " ) ]
IoError ( #[ from ] std ::io ::Error ) ,
}
/// Contains the compiled code to move memory-allocated [DataValue]s to the correct location (e.g.
/// register, stack) dictated by the calling convention before calling a [CompiledFunction]. Without
/// this, it would be quite difficult to correctly place [DataValue]s since both the calling
/// convention and function signature are not known until runtime. See [make_trampoline] for the
/// Cranelift IR used to build this.
pub struct Trampoline {
page : Mmap ,
}
impl Trampoline {
/// Build a new [Trampoline].
pub fn new ( page : Mmap ) -> Self {
Self { page }
}
/// Return a pointer to the compiled code.
fn as_ptr ( & self ) -> * const u8 {
self . page . as_ptr ( )
}
}
/// Container for the compiled code of a [Function]. This wrapper allows users to call the compiled
/// function through the use of a [Trampoline].
///
/// ```
/// use cranelift_filetests::SingleFunctionCompiler;
/// use cranelift_reader::{parse_functions, DataValue};
///
/// let code = "test run \n function %add(i32, i32) -> i32 { block0(v0:i32, v1:i32): v2 = iadd v0, v1 return v2 }".into();
/// let func = parse_functions(code).unwrap().into_iter().nth(0).unwrap();
/// let mut compiler = SingleFunctionCompiler::with_default_host_isa();
/// let compiled_func = compiler.compile(func).unwrap();
///
/// let returned = compiled_func.call(&vec![DataValue::I32(2), DataValue::I32(40)]);
/// assert_eq!(vec![DataValue::I32(42)], returned);
/// ```
pub struct CompiledFunction < 'a > {
page : Mmap ,
signature : Signature ,
trampoline : & 'a Trampoline ,
}
impl < 'a > CompiledFunction < 'a > {
/// Build a new [CompiledFunction].
pub fn new ( page : Mmap , signature : Signature , trampoline : & 'a Trampoline ) -> Self {
Self {
page ,
signature ,
trampoline ,
}
}
/// Return a pointer to the compiled code.
pub fn as_ptr ( & self ) -> * const u8 {
self . page . as_ptr ( )
}
/// Call the [CompiledFunction], passing in [DataValue]s using a compiled [Trampoline].
pub fn call ( & self , arguments : & [ DataValue ] ) -> Vec < DataValue > {
let mut values = UnboxedValues ::make_arguments ( arguments , & self . signature ) ;
let arguments_address = values . as_mut_ptr ( ) ;
let function_address = self . as_ptr ( ) ;
let callable_trampoline : fn ( * const u8 , * mut u128 ) -> ( ) =
unsafe { mem ::transmute ( self . trampoline . as_ptr ( ) ) } ;
callable_trampoline ( function_address , arguments_address ) ;
values . collect_returns ( & self . signature )
}
}
/// A container for laying out the [ValueData]s in memory in a way that the [Trampoline] can
/// understand.
struct UnboxedValues ( Vec < u128 > ) ;
impl UnboxedValues {
/// The size in bytes of each slot location in the allocated [DataValue]s. Though [DataValue]s
/// could be smaller than 16 bytes (e.g. `I16`), this simplifies the creation of the [DataValue]
/// array and could be used to align the slots to the largest used [DataValue] (i.e. 128-bit
/// vectors).
const SLOT_SIZE : usize = 16 ;
/// Build the arguments vector for passing the [DataValue]s into the [Trampoline]. The size of
/// `u128` used here must match [Trampoline::SLOT_SIZE].
pub fn make_arguments ( arguments : & [ DataValue ] , signature : & ir ::Signature ) -> Self {
assert_eq ! ( arguments . len ( ) , signature . params . len ( ) ) ;
let mut values_vec = vec ! [ 0 ; max ( signature . params . len ( ) , signature . returns . len ( ) ) ] ;
// Store the argument values into `values_vec`.
for ( ( arg , slot ) , param ) in arguments . iter ( ) . zip ( & mut values_vec ) . zip ( & signature . params ) {
assert_eq ! (
arg . ty ( ) ,
param . value_type ,
"argument type mismatch: {} != {}" ,
arg . ty ( ) ,
param . value_type
) ;
unsafe {
Self ::write_value_to ( arg , slot ) ;
}
}
Self ( values_vec )
}
/// Return a pointer to the underlying memory for passing to the trampoline.
pub fn as_mut_ptr ( & mut self ) -> * mut u128 {
self . 0. as_mut_ptr ( )
}
/// Collect the returned [DataValue]s into a [Vec]. The size of `u128` used here must match
/// [Trampoline::SLOT_SIZE].
pub fn collect_returns ( & self , signature : & ir ::Signature ) -> Vec < DataValue > {
assert ! ( self . 0. len ( ) > = signature . returns . len ( ) ) ;
let mut returns = Vec ::with_capacity ( signature . returns . len ( ) ) ;
// Extract the returned values from this vector.
for ( slot , param ) in self . 0. iter ( ) . zip ( & signature . returns ) {
let value = unsafe { Self ::read_value_from ( slot , param . value_type ) } ;
returns . push ( value ) ;
}
returns
}
/// Write a [DataValue] to a memory location.
unsafe fn write_value_to ( v : & DataValue , p : * mut u128 ) {
match v {
DataValue ::B ( b ) = > ptr ::write ( p as * mut bool , * b ) ,
DataValue ::I8 ( i ) = > ptr ::write ( p as * mut i8 , * i ) ,
DataValue ::I16 ( i ) = > ptr ::write ( p as * mut i16 , * i ) ,
DataValue ::I32 ( i ) = > ptr ::write ( p as * mut i32 , * i ) ,
DataValue ::I64 ( i ) = > ptr ::write ( p as * mut i64 , * i ) ,
DataValue ::F32 ( f ) = > ptr ::write ( p as * mut f32 , * f ) ,
DataValue ::F64 ( f ) = > ptr ::write ( p as * mut f64 , * f ) ,
DataValue ::V128 ( b ) = > ptr ::write ( p as * mut [ u8 ; 16 ] , * b ) ,
}
}
// set up the context
/// Read a [DataValue] from a memory location using a given [Type].
unsafe fn read_value_from ( p : * const u128 , ty : Type ) -> DataValue {
match ty {
ir ::types ::I8 = > DataValue ::I8 ( ptr ::read ( p as * const i8 ) ) ,
ir ::types ::I16 = > DataValue ::I16 ( ptr ::read ( p as * const i16 ) ) ,
ir ::types ::I32 = > DataValue ::I32 ( ptr ::read ( p as * const i32 ) ) ,
ir ::types ::I64 = > DataValue ::I64 ( ptr ::read ( p as * const i64 ) ) ,
ir ::types ::F32 = > DataValue ::F32 ( ptr ::read ( p as * const f32 ) ) ,
ir ::types ::F64 = > DataValue ::F64 ( ptr ::read ( p as * const f64 ) ) ,
_ if ty . is_bool ( ) = > DataValue ::B ( ptr ::read ( p as * const bool ) ) ,
_ if ty . is_vector ( ) & & ty . bytes ( ) = = 16 = > {
DataValue ::V128 ( ptr ::read ( p as * const [ u8 ; 16 ] ) )
}
_ = > unimplemented ! ( ) ,
}
}
}
/// Compile a [Function] to its executable bytes in memory.
///
/// This currently returns a [Mmap], a type from an external crate, so we wrap this up before
/// exposing it in public APIs.
fn compile ( function : Function , isa : & dyn TargetIsa ) -> Result < Mmap , CompilationError > {
// Set up the context.
let mut context = Context ::new ( ) ;
context . func = func ;
context . func = function ;
// compile and encode the result to machine code
// Compile and encode the result to machine code.
let relocs = & mut NullRelocSink { } ;
let traps = & mut NullTrapSink { } ;
let stackmaps = & mut NullStackmapSink { } ;
let code_info = context
. compile ( self . isa . as_ref ( ) )
. map_err ( | e | e . to_string ( ) ) ? ;
let mut code_page =
MmapMut ::map_anon ( code_info . total_size as usize ) . map_err ( | e | e . to_string ( ) ) ? ;
let code_info = context . compile ( isa ) ? ;
let mut code_page = MmapMut ::map_anon ( code_info . total_size as usize ) ? ;
unsafe {
context . emit_to_memory (
self . isa . as_ref ( ) ,
code_page . as_mut_ptr ( ) ,
relocs ,
traps ,
stackmaps ,
) ;
context . emit_to_memory ( isa , code_page . as_mut_ptr ( ) , relocs , traps , stackmaps ) ;
} ;
let code_page = code_page . make_exec ( ) . map_err ( | e | e . to_string ( ) ) ? ;
let callable_fn : fn ( ) -> bool = unsafe { mem ::transmute ( code_page . as_ptr ( ) ) } ;
let code_page = code_page . make_exec ( ) ? ;
trace ! (
"Compiled function {} with signature {} at: {:p}" ,
context . func . name ,
context . func . signature ,
code_page . as_ptr ( )
) ;
Ok ( code_page )
}
/// Build the Cranelift IR for moving the memory-allocated [DataValue]s to their correct location
/// (e.g. register, stack) prior to calling a [CompiledFunction]. The [Function] returned by
/// [make_trampoline] is compiled to a [Trampoline]. Note that this uses the [TargetIsa]'s default
/// calling convention so we must also check that the [CompiledFunction] has the same calling
/// convention (see [SingleFunctionCompiler::compile]).
fn make_trampoline ( signature : & ir ::Signature , isa : & dyn TargetIsa ) -> Function {
// Create the trampoline signature: (callee_address: pointer, values_vec: pointer) -> ()
let pointer_type = isa . pointer_type ( ) ;
let mut wrapper_sig = ir ::Signature ::new ( isa . frontend_config ( ) . default_call_conv ) ;
wrapper_sig . params . push ( ir ::AbiParam ::new ( pointer_type ) ) ; // Add the `callee_address` parameter.
wrapper_sig . params . push ( ir ::AbiParam ::new ( pointer_type ) ) ; // Add the `values_vec` parameter.
// execute
if callable_fn ( ) {
Ok ( ( ) )
let mut func = ir ::Function ::with_name_signature ( ir ::ExternalName ::user ( 0 , 0 ) , wrapper_sig ) ;
// The trampoline has a single block filled with loads, one call to callee_address, and some loads.
let mut builder_context = FunctionBuilderContext ::new ( ) ;
let mut builder = FunctionBuilder ::new ( & mut func , & mut builder_context ) ;
let block0 = builder . create_block ( ) ;
builder . append_block_params_for_function_params ( block0 ) ;
builder . switch_to_block ( block0 ) ;
builder . seal_block ( block0 ) ;
// Extract the incoming SSA values.
let ( callee_value , values_vec_ptr_val ) = {
let params = builder . func . dfg . block_params ( block0 ) ;
( params [ 0 ] , params [ 1 ] )
} ;
// Load the argument values out of `values_vec`.
let callee_args = signature
. params
. iter ( )
. enumerate ( )
. map ( | ( i , param ) | {
// Calculate the type to load from memory, using integers for booleans (no encodings).
let ty = if param . value_type . is_bool ( ) {
Type ::int ( max ( param . value_type . bits ( ) , 8 ) ) . expect (
"to be able to convert any boolean type to its equal-width integer type" ,
)
} else {
param . value_type
} ;
// Load the value.
let loaded = builder . ins ( ) . load (
ty ,
ir ::MemFlags ::trusted ( ) ,
values_vec_ptr_val ,
( i * UnboxedValues ::SLOT_SIZE ) as i32 ,
) ;
// For booleans, we want to type-convert the loaded integer into a boolean and ensure
// that we are using the architecture's canonical boolean representation (presumably
// comparison will emit this).
if param . value_type . is_bool ( ) {
builder . ins ( ) . icmp_imm ( IntCC ::NotEqual , loaded , 0 )
} else {
Err ( format ! ( "Failed: {}" , context . func . name . to_string ( ) ) )
loaded
}
} )
. collect ::< Vec < _ > > ( ) ;
// Call the passed function.
let new_sig = builder . import_signature ( signature . clone ( ) ) ;
let call = builder
. ins ( )
. call_indirect ( new_sig , callee_value , & callee_args ) ;
// Store the return values into `values_vec`.
let results = builder . func . dfg . inst_results ( call ) . to_vec ( ) ;
for ( ( i , value ) , param ) in results . iter ( ) . enumerate ( ) . zip ( & signature . returns ) {
// Before storing return values, we convert booleans to their integer representation.
let value = if param . value_type . is_bool ( ) {
let ty = Type ::int ( max ( param . value_type . bits ( ) , 8 ) )
. expect ( "to be able to convert any boolean type to its equal-width integer type" ) ;
builder . ins ( ) . bint ( ty , * value )
} else {
* value
} ;
// Store the value.
builder . ins ( ) . store (
ir ::MemFlags ::trusted ( ) ,
value ,
values_vec_ptr_val ,
( i * UnboxedValues ::SLOT_SIZE ) as i32 ,
) ;
}
builder . ins ( ) . return_ ( & [ ] ) ;
builder . finalize ( ) ;
func
}
#[ cfg(test) ]
mod test {
use super ::* ;
use cranelift_reader ::{ parse_test , ParseOptions } ;
use cranelift_reader ::{ parse_functions , parse_test , ParseOptions } ;
fn parse ( code : & str ) -> Function {
parse_functions ( code ) . unwrap ( ) . into_iter ( ) . nth ( 0 ) . unwrap ( )
}
#[ test ]
fn nop ( ) {
@ -111,7 +407,42 @@ mod test {
let function = test_file . functions [ 0 ] . 0. clone ( ) ;
// execute function
let runner = FunctionRunner ::with_default_host_isa ( function ) ;
runner . run ( ) . unwrap ( ) // will panic if execution fails
let mut compiler = SingleFunctionCompiler ::with_default_host_isa ( ) ;
let compiled_function = compiler . compile ( function ) . unwrap ( ) ;
let returned = compiled_function . call ( & [ ] ) ;
assert_eq ! ( returned , vec ! [ DataValue ::B ( true ) ] )
}
#[ test ]
fn trampolines ( ) {
let function = parse (
"
function % test ( f32 , i8 , i64x2 , b1 ) -> f32x4 , b64 {
block0 ( v0 : f32 , v1 : i8 , v2 : i64x2 , v3 : b1 ) :
v4 = vconst . f32x4 [ 0x0 . 1 0x0 . 2 0x0 . 3 0x0 . 4 ]
v5 = bconst . b64 true
return v4 , v5
} " ,
) ;
let compiler = SingleFunctionCompiler ::with_default_host_isa ( ) ;
let trampoline = make_trampoline ( & function . signature , compiler . isa . as_ref ( ) ) ;
assert ! ( format ! ( "{}" , trampoline ) . ends_with (
" sig0 = ( f32 , i8 , i64x2 , b1 ) -> f32x4 , b64 fast
block0 ( v0 : i64 , v1 : i64 ) :
v2 = load . f32 notrap aligned v1
v3 = load . i8 notrap aligned v1 + 16
v4 = load . i64x2 notrap aligned v1 + 32
v5 = load . i8 notrap aligned v1 + 48
v6 = icmp_imm ne v5 , 0
v7 , v8 = call_indirect sig0 , v0 ( v2 , v3 , v4 , v6 )
store notrap aligned v7 , v1
v9 = bint . i64 v8
store notrap aligned v9 , v1 + 16
return
}
"
) ) ;
}
}