Auto merge of #3593 - RalfJung:native-lib, r=RalfJung
rename 'extern-so' to 'native-lib' Calling "extern" functions is not super clear IMO (extern to what), but saying that we are calling "native" functions from inside the interpreter makes it very clear I think.
This commit is contained in:
commit
6f4c7d997e
2
src/tools/miri/.gitignore
vendored
2
src/tools/miri/.gitignore
vendored
@ -9,5 +9,5 @@ tex/*/out
|
||||
perf.data
|
||||
perf.data.old
|
||||
flamegraph.svg
|
||||
tests/extern-so/libtestlib.so
|
||||
tests/native-lib/libtestlib.so
|
||||
.auto-*
|
||||
|
@ -374,17 +374,17 @@ to Miri failing to detect cases of undefined behavior in a program.
|
||||
this flag is **unsound**.
|
||||
* `-Zmiri-disable-weak-memory-emulation` disables the emulation of some C++11 weak
|
||||
memory effects.
|
||||
* `-Zmiri-extern-so-file=<path to a shared object file>` is an experimental flag for providing support
|
||||
for FFI calls. Functions not provided by that file are still executed via the usual Miri shims.
|
||||
**WARNING**: If an invalid/incorrect `.so` file is specified, this can cause undefined behaviour in Miri itself!
|
||||
And of course, Miri cannot do any checks on the actions taken by the external code.
|
||||
* `-Zmiri-native-lib=<path to a shared object file>` is an experimental flag for providing support
|
||||
for calling native functions from inside the interpreter via FFI. Functions not provided by that
|
||||
file are still executed via the usual Miri shims.
|
||||
**WARNING**: If an invalid/incorrect `.so` file is specified, this can cause Undefined Behavior in Miri itself!
|
||||
And of course, Miri cannot do any checks on the actions taken by the native code.
|
||||
Note that Miri has its own handling of file descriptors, so if you want to replace *some* functions
|
||||
working on file descriptors, you will have to replace *all* of them, or the two kinds of
|
||||
file descriptors will be mixed up.
|
||||
This is **work in progress**; currently, only integer arguments and return values are
|
||||
supported (and no, pointer/integer casts to work around this limitation will not work;
|
||||
they will fail horribly). It also only works on unix hosts for now.
|
||||
Follow [the discussion on supporting other types](https://github.com/rust-lang/miri/issues/2365).
|
||||
they will fail horribly). It also only works on Linux hosts for now.
|
||||
* `-Zmiri-measureme=<name>` enables `measureme` profiling for the interpreted program.
|
||||
This can be used to find which parts of your program are executing slowly under Miri.
|
||||
The profile is written out to a file inside a directory called `<name>`, and can be processed
|
||||
|
@ -575,18 +575,15 @@ fn main() {
|
||||
"full" => BacktraceStyle::Full,
|
||||
_ => show_error!("-Zmiri-backtrace may only be 0, 1, or full"),
|
||||
};
|
||||
} else if let Some(param) = arg.strip_prefix("-Zmiri-extern-so-file=") {
|
||||
} else if let Some(param) = arg.strip_prefix("-Zmiri-native-lib=") {
|
||||
let filename = param.to_string();
|
||||
if std::path::Path::new(&filename).exists() {
|
||||
if let Some(other_filename) = miri_config.external_so_file {
|
||||
show_error!(
|
||||
"-Zmiri-extern-so-file is already set to {}",
|
||||
other_filename.display()
|
||||
);
|
||||
if let Some(other_filename) = miri_config.native_lib {
|
||||
show_error!("-Zmiri-native-lib is already set to {}", other_filename.display());
|
||||
}
|
||||
miri_config.external_so_file = Some(filename.into());
|
||||
miri_config.native_lib = Some(filename.into());
|
||||
} else {
|
||||
show_error!("-Zmiri-extern-so-file `{}` does not exist", filename);
|
||||
show_error!("-Zmiri-native-lib `{}` does not exist", filename);
|
||||
}
|
||||
} else if let Some(param) = arg.strip_prefix("-Zmiri-num-cpus=") {
|
||||
let num_cpus = param
|
||||
|
@ -142,8 +142,8 @@ pub struct MiriConfig {
|
||||
/// Whether Stacked Borrows and Tree Borrows retagging should recurse into fields of datatypes.
|
||||
pub retag_fields: RetagFields,
|
||||
/// The location of a shared object file to load when calling external functions
|
||||
/// FIXME! consider allowing users to specify paths to multiple SO files, or to a directory
|
||||
pub external_so_file: Option<PathBuf>,
|
||||
/// FIXME! consider allowing users to specify paths to multiple files, or to a directory
|
||||
pub native_lib: Option<PathBuf>,
|
||||
/// Run a garbage collector for BorTags every N basic blocks.
|
||||
pub gc_interval: u32,
|
||||
/// The number of CPUs to be reported by miri.
|
||||
@ -188,7 +188,7 @@ fn default() -> MiriConfig {
|
||||
preemption_rate: 0.01, // 1%
|
||||
report_progress: None,
|
||||
retag_fields: RetagFields::Yes,
|
||||
external_so_file: None,
|
||||
native_lib: None,
|
||||
gc_interval: 10_000,
|
||||
num_cpus: 1,
|
||||
page_size: None,
|
||||
|
@ -535,11 +535,11 @@ pub struct MiriMachine<'mir, 'tcx> {
|
||||
// The total number of blocks that have been executed.
|
||||
pub(crate) basic_block_count: u64,
|
||||
|
||||
/// Handle of the optional shared object file for external functions.
|
||||
/// Handle of the optional shared object file for native functions.
|
||||
#[cfg(target_os = "linux")]
|
||||
pub external_so_lib: Option<(libloading::Library, std::path::PathBuf)>,
|
||||
pub native_lib: Option<(libloading::Library, std::path::PathBuf)>,
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
pub external_so_lib: Option<!>,
|
||||
pub native_lib: Option<!>,
|
||||
|
||||
/// Run a garbage collector for BorTags every N basic blocks.
|
||||
pub(crate) gc_interval: u32,
|
||||
@ -665,7 +665,7 @@ pub(crate) fn new(config: &MiriConfig, layout_cx: LayoutCx<'tcx, TyCtxt<'tcx>>)
|
||||
basic_block_count: 0,
|
||||
clock: Clock::new(config.isolated_op == IsolatedOp::Allow),
|
||||
#[cfg(target_os = "linux")]
|
||||
external_so_lib: config.external_so_file.as_ref().map(|lib_file_path| {
|
||||
native_lib: config.native_lib.as_ref().map(|lib_file_path| {
|
||||
let target_triple = layout_cx.tcx.sess.opts.target_triple.triple();
|
||||
// Check if host target == the session target.
|
||||
if env!("TARGET") != target_triple {
|
||||
@ -687,7 +687,7 @@ pub(crate) fn new(config: &MiriConfig, layout_cx: LayoutCx<'tcx, TyCtxt<'tcx>>)
|
||||
)
|
||||
}),
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
external_so_lib: config.external_so_file.as_ref().map(|_| {
|
||||
native_lib: config.native_lib.as_ref().map(|_| {
|
||||
panic!("loading external .so files is only supported on Linux")
|
||||
}),
|
||||
gc_interval: config.gc_interval,
|
||||
@ -802,7 +802,7 @@ fn visit_provenance(&self, visit: &mut VisitWith<'_>) {
|
||||
preemption_rate: _,
|
||||
report_progress: _,
|
||||
basic_block_count: _,
|
||||
external_so_lib: _,
|
||||
native_lib: _,
|
||||
gc_interval: _,
|
||||
since_gc: _,
|
||||
num_cpus: _,
|
||||
|
@ -1,289 +0,0 @@
|
||||
use libffi::{high::call as ffi, low::CodePtr};
|
||||
use std::ops::Deref;
|
||||
|
||||
use rustc_middle::ty::{self as ty, IntTy, Ty, UintTy};
|
||||
use rustc_span::Symbol;
|
||||
use rustc_target::abi::HasDataLayout;
|
||||
|
||||
use crate::*;
|
||||
|
||||
impl<'mir, 'tcx: 'mir> EvalContextExt<'mir, 'tcx> for crate::MiriInterpCx<'mir, 'tcx> {}
|
||||
|
||||
pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
|
||||
/// Extract the scalar value from the result of reading a scalar from the machine,
|
||||
/// and convert it to a `CArg`.
|
||||
fn scalar_to_carg(
|
||||
k: Scalar<Provenance>,
|
||||
arg_type: Ty<'tcx>,
|
||||
cx: &impl HasDataLayout,
|
||||
) -> InterpResult<'tcx, CArg> {
|
||||
match arg_type.kind() {
|
||||
// If the primitive provided can be converted to a type matching the type pattern
|
||||
// then create a `CArg` of this primitive value with the corresponding `CArg` constructor.
|
||||
// the ints
|
||||
ty::Int(IntTy::I8) => {
|
||||
return Ok(CArg::Int8(k.to_i8()?));
|
||||
}
|
||||
ty::Int(IntTy::I16) => {
|
||||
return Ok(CArg::Int16(k.to_i16()?));
|
||||
}
|
||||
ty::Int(IntTy::I32) => {
|
||||
return Ok(CArg::Int32(k.to_i32()?));
|
||||
}
|
||||
ty::Int(IntTy::I64) => {
|
||||
return Ok(CArg::Int64(k.to_i64()?));
|
||||
}
|
||||
ty::Int(IntTy::Isize) => {
|
||||
// This will fail if host != target, but then the entire FFI thing probably won't work well
|
||||
// in that situation.
|
||||
return Ok(CArg::ISize(k.to_target_isize(cx)?.try_into().unwrap()));
|
||||
}
|
||||
// the uints
|
||||
ty::Uint(UintTy::U8) => {
|
||||
return Ok(CArg::UInt8(k.to_u8()?));
|
||||
}
|
||||
ty::Uint(UintTy::U16) => {
|
||||
return Ok(CArg::UInt16(k.to_u16()?));
|
||||
}
|
||||
ty::Uint(UintTy::U32) => {
|
||||
return Ok(CArg::UInt32(k.to_u32()?));
|
||||
}
|
||||
ty::Uint(UintTy::U64) => {
|
||||
return Ok(CArg::UInt64(k.to_u64()?));
|
||||
}
|
||||
ty::Uint(UintTy::Usize) => {
|
||||
// This will fail if host != target, but then the entire FFI thing probably won't work well
|
||||
// in that situation.
|
||||
return Ok(CArg::USize(k.to_target_usize(cx)?.try_into().unwrap()));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
// If no primitives were returned then we have an unsupported type.
|
||||
throw_unsup_format!(
|
||||
"unsupported scalar argument type to external C function: {:?}",
|
||||
arg_type
|
||||
);
|
||||
}
|
||||
|
||||
/// Call external C function and
|
||||
/// store output, depending on return type in the function signature.
|
||||
fn call_external_c_and_store_return<'a>(
|
||||
&mut self,
|
||||
link_name: Symbol,
|
||||
dest: &MPlaceTy<'tcx, Provenance>,
|
||||
ptr: CodePtr,
|
||||
libffi_args: Vec<libffi::high::Arg<'a>>,
|
||||
) -> InterpResult<'tcx, ()> {
|
||||
let this = self.eval_context_mut();
|
||||
|
||||
// Unsafe because of the call to external C code.
|
||||
// Because this is calling a C function it is not necessarily sound,
|
||||
// but there is no way around this and we've checked as much as we can.
|
||||
unsafe {
|
||||
// If the return type of a function is a primitive integer type,
|
||||
// then call the function (`ptr`) with arguments `libffi_args`, store the return value as the specified
|
||||
// primitive integer type, and then write this value out to the miri memory as an integer.
|
||||
match dest.layout.ty.kind() {
|
||||
// ints
|
||||
ty::Int(IntTy::I8) => {
|
||||
let x = ffi::call::<i8>(ptr, libffi_args.as_slice());
|
||||
this.write_int(x, dest)?;
|
||||
return Ok(());
|
||||
}
|
||||
ty::Int(IntTy::I16) => {
|
||||
let x = ffi::call::<i16>(ptr, libffi_args.as_slice());
|
||||
this.write_int(x, dest)?;
|
||||
return Ok(());
|
||||
}
|
||||
ty::Int(IntTy::I32) => {
|
||||
let x = ffi::call::<i32>(ptr, libffi_args.as_slice());
|
||||
this.write_int(x, dest)?;
|
||||
return Ok(());
|
||||
}
|
||||
ty::Int(IntTy::I64) => {
|
||||
let x = ffi::call::<i64>(ptr, libffi_args.as_slice());
|
||||
this.write_int(x, dest)?;
|
||||
return Ok(());
|
||||
}
|
||||
ty::Int(IntTy::Isize) => {
|
||||
let x = ffi::call::<isize>(ptr, libffi_args.as_slice());
|
||||
// `isize` doesn't `impl Into<i128>`, so convert manually.
|
||||
// Convert to `i64` since this covers both 32- and 64-bit machines.
|
||||
this.write_int(i64::try_from(x).unwrap(), dest)?;
|
||||
return Ok(());
|
||||
}
|
||||
// uints
|
||||
ty::Uint(UintTy::U8) => {
|
||||
let x = ffi::call::<u8>(ptr, libffi_args.as_slice());
|
||||
this.write_int(x, dest)?;
|
||||
return Ok(());
|
||||
}
|
||||
ty::Uint(UintTy::U16) => {
|
||||
let x = ffi::call::<u16>(ptr, libffi_args.as_slice());
|
||||
this.write_int(x, dest)?;
|
||||
return Ok(());
|
||||
}
|
||||
ty::Uint(UintTy::U32) => {
|
||||
let x = ffi::call::<u32>(ptr, libffi_args.as_slice());
|
||||
this.write_int(x, dest)?;
|
||||
return Ok(());
|
||||
}
|
||||
ty::Uint(UintTy::U64) => {
|
||||
let x = ffi::call::<u64>(ptr, libffi_args.as_slice());
|
||||
this.write_int(x, dest)?;
|
||||
return Ok(());
|
||||
}
|
||||
ty::Uint(UintTy::Usize) => {
|
||||
let x = ffi::call::<usize>(ptr, libffi_args.as_slice());
|
||||
// `usize` doesn't `impl Into<i128>`, so convert manually.
|
||||
// Convert to `u64` since this covers both 32- and 64-bit machines.
|
||||
this.write_int(u64::try_from(x).unwrap(), dest)?;
|
||||
return Ok(());
|
||||
}
|
||||
// Functions with no declared return type (i.e., the default return)
|
||||
// have the output_type `Tuple([])`.
|
||||
ty::Tuple(t_list) =>
|
||||
if t_list.len() == 0 {
|
||||
ffi::call::<()>(ptr, libffi_args.as_slice());
|
||||
return Ok(());
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
// FIXME ellen! deal with all the other return types
|
||||
throw_unsup_format!("unsupported return type to external C function: {:?}", link_name);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the pointer to the function of the specified name in the shared object file,
|
||||
/// if it exists. The function must be in the shared object file specified: we do *not*
|
||||
/// return pointers to functions in dependencies of the library.
|
||||
fn get_func_ptr_explicitly_from_lib(&mut self, link_name: Symbol) -> Option<CodePtr> {
|
||||
let this = self.eval_context_mut();
|
||||
// Try getting the function from the shared library.
|
||||
// On windows `_lib_path` will be unused, hence the name starting with `_`.
|
||||
let (lib, _lib_path) = this.machine.external_so_lib.as_ref().unwrap();
|
||||
let func: libloading::Symbol<'_, unsafe extern "C" fn()> = unsafe {
|
||||
match lib.get(link_name.as_str().as_bytes()) {
|
||||
Ok(x) => x,
|
||||
Err(_) => {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// FIXME: this is a hack!
|
||||
// The `libloading` crate will automatically load system libraries like `libc`.
|
||||
// On linux `libloading` is based on `dlsym`: https://docs.rs/libloading/0.7.3/src/libloading/os/unix/mod.rs.html#202
|
||||
// and `dlsym`(https://linux.die.net/man/3/dlsym) looks through the dependency tree of the
|
||||
// library if it can't find the symbol in the library itself.
|
||||
// So, in order to check if the function was actually found in the specified
|
||||
// `machine.external_so_lib` we need to check its `dli_fname` and compare it to
|
||||
// the specified SO file path.
|
||||
// This code is a reimplementation of the mechanism for getting `dli_fname` in `libloading`,
|
||||
// from: https://docs.rs/libloading/0.7.3/src/libloading/os/unix/mod.rs.html#411
|
||||
// using the `libc` crate where this interface is public.
|
||||
// No `libc::dladdr` on windows.
|
||||
let mut info = std::mem::MaybeUninit::<libc::Dl_info>::uninit();
|
||||
unsafe {
|
||||
if libc::dladdr(*func.deref() as *const _, info.as_mut_ptr()) != 0 {
|
||||
if std::ffi::CStr::from_ptr(info.assume_init().dli_fname).to_str().unwrap()
|
||||
!= _lib_path.to_str().unwrap()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Return a pointer to the function.
|
||||
Some(CodePtr(*func.deref() as *mut _))
|
||||
}
|
||||
|
||||
/// Call specified external C function, with supplied arguments.
|
||||
/// Need to convert all the arguments from their hir representations to
|
||||
/// a form compatible with C (through `libffi` call).
|
||||
/// Then, convert return from the C call into a corresponding form that
|
||||
/// can be stored in Miri internal memory.
|
||||
fn call_external_c_fct(
|
||||
&mut self,
|
||||
link_name: Symbol,
|
||||
dest: &MPlaceTy<'tcx, Provenance>,
|
||||
args: &[OpTy<'tcx, Provenance>],
|
||||
) -> InterpResult<'tcx, bool> {
|
||||
// Get the pointer to the function in the shared object file if it exists.
|
||||
let code_ptr = match self.get_func_ptr_explicitly_from_lib(link_name) {
|
||||
Some(ptr) => ptr,
|
||||
None => {
|
||||
// Shared object file does not export this function -- try the shims next.
|
||||
return Ok(false);
|
||||
}
|
||||
};
|
||||
|
||||
let this = self.eval_context_mut();
|
||||
|
||||
// Get the function arguments, and convert them to `libffi`-compatible form.
|
||||
let mut libffi_args = Vec::<CArg>::with_capacity(args.len());
|
||||
for cur_arg in args.iter() {
|
||||
libffi_args.push(Self::scalar_to_carg(
|
||||
this.read_scalar(cur_arg)?,
|
||||
cur_arg.layout.ty,
|
||||
this,
|
||||
)?);
|
||||
}
|
||||
|
||||
// Convert them to `libffi::high::Arg` type.
|
||||
let libffi_args = libffi_args
|
||||
.iter()
|
||||
.map(|cur_arg| cur_arg.arg_downcast())
|
||||
.collect::<Vec<libffi::high::Arg<'_>>>();
|
||||
|
||||
// Call the function and store output, depending on return type in the function signature.
|
||||
self.call_external_c_and_store_return(link_name, dest, code_ptr, libffi_args)?;
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
/// Enum of supported arguments to external C functions.
|
||||
// We introduce this enum instead of just calling `ffi::arg` and storing a list
|
||||
// of `libffi::high::Arg` directly, because the `libffi::high::Arg` just wraps a reference
|
||||
// to the value it represents: https://docs.rs/libffi/latest/libffi/high/call/struct.Arg.html
|
||||
// and we need to store a copy of the value, and pass a reference to this copy to C instead.
|
||||
pub enum CArg {
|
||||
/// 8-bit signed integer.
|
||||
Int8(i8),
|
||||
/// 16-bit signed integer.
|
||||
Int16(i16),
|
||||
/// 32-bit signed integer.
|
||||
Int32(i32),
|
||||
/// 64-bit signed integer.
|
||||
Int64(i64),
|
||||
/// isize.
|
||||
ISize(isize),
|
||||
/// 8-bit unsigned integer.
|
||||
UInt8(u8),
|
||||
/// 16-bit unsigned integer.
|
||||
UInt16(u16),
|
||||
/// 32-bit unsigned integer.
|
||||
UInt32(u32),
|
||||
/// 64-bit unsigned integer.
|
||||
UInt64(u64),
|
||||
/// usize.
|
||||
USize(usize),
|
||||
}
|
||||
|
||||
impl<'a> CArg {
|
||||
/// Convert a `CArg` to a `libffi` argument type.
|
||||
fn arg_downcast(&'a self) -> libffi::high::Arg<'a> {
|
||||
match self {
|
||||
CArg::Int8(i) => ffi::arg(i),
|
||||
CArg::Int16(i) => ffi::arg(i),
|
||||
CArg::Int32(i) => ffi::arg(i),
|
||||
CArg::Int64(i) => ffi::arg(i),
|
||||
CArg::ISize(i) => ffi::arg(i),
|
||||
CArg::UInt8(i) => ffi::arg(i),
|
||||
CArg::UInt16(i) => ffi::arg(i),
|
||||
CArg::UInt32(i) => ffi::arg(i),
|
||||
CArg::UInt64(i) => ffi::arg(i),
|
||||
CArg::USize(i) => ffi::arg(i),
|
||||
}
|
||||
}
|
||||
}
|
@ -211,12 +211,12 @@ fn emulate_foreign_item_inner(
|
||||
|
||||
// First deal with any external C functions in linked .so file.
|
||||
#[cfg(target_os = "linux")]
|
||||
if this.machine.external_so_lib.as_ref().is_some() {
|
||||
use crate::shims::ffi_support::EvalContextExt as _;
|
||||
if this.machine.native_lib.as_ref().is_some() {
|
||||
use crate::shims::native_lib::EvalContextExt as _;
|
||||
// An Ok(false) here means that the function being called was not exported
|
||||
// by the specified `.so` file; we should continue and check if it corresponds to
|
||||
// a provided shim.
|
||||
if this.call_external_c_fct(link_name, dest, args)? {
|
||||
if this.call_native_fn(link_name, dest, args)? {
|
||||
return Ok(EmulateItemResult::NeedsJumping);
|
||||
}
|
||||
}
|
||||
|
@ -2,9 +2,9 @@
|
||||
|
||||
mod alloc;
|
||||
mod backtrace;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod ffi_support;
|
||||
pub mod foreign_items;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod native_lib;
|
||||
pub mod unix;
|
||||
pub mod windows;
|
||||
mod x86;
|
||||
|
242
src/tools/miri/src/shims/native_lib.rs
Normal file
242
src/tools/miri/src/shims/native_lib.rs
Normal file
@ -0,0 +1,242 @@
|
||||
//! Implements calling functions from a native library.
|
||||
use libffi::{high::call as ffi, low::CodePtr};
|
||||
use std::ops::Deref;
|
||||
|
||||
use rustc_middle::ty::{self as ty, IntTy, UintTy};
|
||||
use rustc_span::Symbol;
|
||||
use rustc_target::abi::{Abi, HasDataLayout};
|
||||
|
||||
use crate::*;
|
||||
|
||||
impl<'mir, 'tcx: 'mir> EvalContextExtPriv<'mir, 'tcx> for crate::MiriInterpCx<'mir, 'tcx> {}
|
||||
trait EvalContextExtPriv<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
|
||||
/// Call native host function and return the output as an immediate.
|
||||
fn call_native_with_args<'a>(
|
||||
&mut self,
|
||||
link_name: Symbol,
|
||||
dest: &MPlaceTy<'tcx, Provenance>,
|
||||
ptr: CodePtr,
|
||||
libffi_args: Vec<libffi::high::Arg<'a>>,
|
||||
) -> InterpResult<'tcx, ImmTy<'tcx, Provenance>> {
|
||||
let this = self.eval_context_mut();
|
||||
|
||||
// Call the function (`ptr`) with arguments `libffi_args`, and obtain the return value
|
||||
// as the specified primitive integer type
|
||||
let scalar = match dest.layout.ty.kind() {
|
||||
// ints
|
||||
ty::Int(IntTy::I8) => {
|
||||
// Unsafe because of the call to native code.
|
||||
// Because this is calling a C function it is not necessarily sound,
|
||||
// but there is no way around this and we've checked as much as we can.
|
||||
let x = unsafe { ffi::call::<i8>(ptr, libffi_args.as_slice()) };
|
||||
Scalar::from_i8(x)
|
||||
}
|
||||
ty::Int(IntTy::I16) => {
|
||||
let x = unsafe { ffi::call::<i16>(ptr, libffi_args.as_slice()) };
|
||||
Scalar::from_i16(x)
|
||||
}
|
||||
ty::Int(IntTy::I32) => {
|
||||
let x = unsafe { ffi::call::<i32>(ptr, libffi_args.as_slice()) };
|
||||
Scalar::from_i32(x)
|
||||
}
|
||||
ty::Int(IntTy::I64) => {
|
||||
let x = unsafe { ffi::call::<i64>(ptr, libffi_args.as_slice()) };
|
||||
Scalar::from_i64(x)
|
||||
}
|
||||
ty::Int(IntTy::Isize) => {
|
||||
let x = unsafe { ffi::call::<isize>(ptr, libffi_args.as_slice()) };
|
||||
Scalar::from_target_isize(x.try_into().unwrap(), this)
|
||||
}
|
||||
// uints
|
||||
ty::Uint(UintTy::U8) => {
|
||||
let x = unsafe { ffi::call::<u8>(ptr, libffi_args.as_slice()) };
|
||||
Scalar::from_u8(x)
|
||||
}
|
||||
ty::Uint(UintTy::U16) => {
|
||||
let x = unsafe { ffi::call::<u16>(ptr, libffi_args.as_slice()) };
|
||||
Scalar::from_u16(x)
|
||||
}
|
||||
ty::Uint(UintTy::U32) => {
|
||||
let x = unsafe { ffi::call::<u32>(ptr, libffi_args.as_slice()) };
|
||||
Scalar::from_u32(x)
|
||||
}
|
||||
ty::Uint(UintTy::U64) => {
|
||||
let x = unsafe { ffi::call::<u64>(ptr, libffi_args.as_slice()) };
|
||||
Scalar::from_u64(x)
|
||||
}
|
||||
ty::Uint(UintTy::Usize) => {
|
||||
let x = unsafe { ffi::call::<usize>(ptr, libffi_args.as_slice()) };
|
||||
Scalar::from_target_usize(x.try_into().unwrap(), this)
|
||||
}
|
||||
// Functions with no declared return type (i.e., the default return)
|
||||
// have the output_type `Tuple([])`.
|
||||
ty::Tuple(t_list) if t_list.len() == 0 => {
|
||||
unsafe { ffi::call::<()>(ptr, libffi_args.as_slice()) };
|
||||
return Ok(ImmTy::uninit(dest.layout));
|
||||
}
|
||||
_ => throw_unsup_format!("unsupported return type for native call: {:?}", link_name),
|
||||
};
|
||||
Ok(ImmTy::from_scalar(scalar, dest.layout))
|
||||
}
|
||||
|
||||
/// Get the pointer to the function of the specified name in the shared object file,
|
||||
/// if it exists. The function must be in the shared object file specified: we do *not*
|
||||
/// return pointers to functions in dependencies of the library.
|
||||
fn get_func_ptr_explicitly_from_lib(&mut self, link_name: Symbol) -> Option<CodePtr> {
|
||||
let this = self.eval_context_mut();
|
||||
// Try getting the function from the shared library.
|
||||
// On windows `_lib_path` will be unused, hence the name starting with `_`.
|
||||
let (lib, _lib_path) = this.machine.native_lib.as_ref().unwrap();
|
||||
let func: libloading::Symbol<'_, unsafe extern "C" fn()> = unsafe {
|
||||
match lib.get(link_name.as_str().as_bytes()) {
|
||||
Ok(x) => x,
|
||||
Err(_) => {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// FIXME: this is a hack!
|
||||
// The `libloading` crate will automatically load system libraries like `libc`.
|
||||
// On linux `libloading` is based on `dlsym`: https://docs.rs/libloading/0.7.3/src/libloading/os/unix/mod.rs.html#202
|
||||
// and `dlsym`(https://linux.die.net/man/3/dlsym) looks through the dependency tree of the
|
||||
// library if it can't find the symbol in the library itself.
|
||||
// So, in order to check if the function was actually found in the specified
|
||||
// `machine.external_so_lib` we need to check its `dli_fname` and compare it to
|
||||
// the specified SO file path.
|
||||
// This code is a reimplementation of the mechanism for getting `dli_fname` in `libloading`,
|
||||
// from: https://docs.rs/libloading/0.7.3/src/libloading/os/unix/mod.rs.html#411
|
||||
// using the `libc` crate where this interface is public.
|
||||
let mut info = std::mem::MaybeUninit::<libc::Dl_info>::uninit();
|
||||
unsafe {
|
||||
if libc::dladdr(*func.deref() as *const _, info.as_mut_ptr()) != 0 {
|
||||
if std::ffi::CStr::from_ptr(info.assume_init().dli_fname).to_str().unwrap()
|
||||
!= _lib_path.to_str().unwrap()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Return a pointer to the function.
|
||||
Some(CodePtr(*func.deref() as *mut _))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'mir, 'tcx: 'mir> EvalContextExt<'mir, 'tcx> for crate::MiriInterpCx<'mir, 'tcx> {}
|
||||
pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
|
||||
/// Call the native host function, with supplied arguments.
|
||||
/// Needs to convert all the arguments from their Miri representations to
|
||||
/// a native form (through `libffi` call).
|
||||
/// Then, convert the return value from the native form into something that
|
||||
/// can be stored in Miri's internal memory.
|
||||
fn call_native_fn(
|
||||
&mut self,
|
||||
link_name: Symbol,
|
||||
dest: &MPlaceTy<'tcx, Provenance>,
|
||||
args: &[OpTy<'tcx, Provenance>],
|
||||
) -> InterpResult<'tcx, bool> {
|
||||
let this = self.eval_context_mut();
|
||||
// Get the pointer to the function in the shared object file if it exists.
|
||||
let code_ptr = match this.get_func_ptr_explicitly_from_lib(link_name) {
|
||||
Some(ptr) => ptr,
|
||||
None => {
|
||||
// Shared object file does not export this function -- try the shims next.
|
||||
return Ok(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Get the function arguments, and convert them to `libffi`-compatible form.
|
||||
let mut libffi_args = Vec::<CArg>::with_capacity(args.len());
|
||||
for arg in args.iter() {
|
||||
if !matches!(arg.layout.abi, Abi::Scalar(_)) {
|
||||
throw_unsup_format!("only scalar argument types are support for native calls")
|
||||
}
|
||||
libffi_args.push(imm_to_carg(this.read_immediate(arg)?, this)?);
|
||||
}
|
||||
|
||||
// Convert them to `libffi::high::Arg` type.
|
||||
let libffi_args = libffi_args
|
||||
.iter()
|
||||
.map(|arg| arg.arg_downcast())
|
||||
.collect::<Vec<libffi::high::Arg<'_>>>();
|
||||
|
||||
// Call the function and store output, depending on return type in the function signature.
|
||||
let ret = this.call_native_with_args(link_name, dest, code_ptr, libffi_args)?;
|
||||
this.write_immediate(*ret, dest)?;
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
/// Enum of supported arguments to external C functions.
|
||||
// We introduce this enum instead of just calling `ffi::arg` and storing a list
|
||||
// of `libffi::high::Arg` directly, because the `libffi::high::Arg` just wraps a reference
|
||||
// to the value it represents: https://docs.rs/libffi/latest/libffi/high/call/struct.Arg.html
|
||||
// and we need to store a copy of the value, and pass a reference to this copy to C instead.
|
||||
enum CArg {
|
||||
/// 8-bit signed integer.
|
||||
Int8(i8),
|
||||
/// 16-bit signed integer.
|
||||
Int16(i16),
|
||||
/// 32-bit signed integer.
|
||||
Int32(i32),
|
||||
/// 64-bit signed integer.
|
||||
Int64(i64),
|
||||
/// isize.
|
||||
ISize(isize),
|
||||
/// 8-bit unsigned integer.
|
||||
UInt8(u8),
|
||||
/// 16-bit unsigned integer.
|
||||
UInt16(u16),
|
||||
/// 32-bit unsigned integer.
|
||||
UInt32(u32),
|
||||
/// 64-bit unsigned integer.
|
||||
UInt64(u64),
|
||||
/// usize.
|
||||
USize(usize),
|
||||
}
|
||||
|
||||
impl<'a> CArg {
|
||||
/// Convert a `CArg` to a `libffi` argument type.
|
||||
fn arg_downcast(&'a self) -> libffi::high::Arg<'a> {
|
||||
match self {
|
||||
CArg::Int8(i) => ffi::arg(i),
|
||||
CArg::Int16(i) => ffi::arg(i),
|
||||
CArg::Int32(i) => ffi::arg(i),
|
||||
CArg::Int64(i) => ffi::arg(i),
|
||||
CArg::ISize(i) => ffi::arg(i),
|
||||
CArg::UInt8(i) => ffi::arg(i),
|
||||
CArg::UInt16(i) => ffi::arg(i),
|
||||
CArg::UInt32(i) => ffi::arg(i),
|
||||
CArg::UInt64(i) => ffi::arg(i),
|
||||
CArg::USize(i) => ffi::arg(i),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract the scalar value from the result of reading a scalar from the machine,
|
||||
/// and convert it to a `CArg`.
|
||||
fn imm_to_carg<'tcx>(
|
||||
v: ImmTy<'tcx, Provenance>,
|
||||
cx: &impl HasDataLayout,
|
||||
) -> InterpResult<'tcx, CArg> {
|
||||
Ok(match v.layout.ty.kind() {
|
||||
// If the primitive provided can be converted to a type matching the type pattern
|
||||
// then create a `CArg` of this primitive value with the corresponding `CArg` constructor.
|
||||
// the ints
|
||||
ty::Int(IntTy::I8) => CArg::Int8(v.to_scalar().to_i8()?),
|
||||
ty::Int(IntTy::I16) => CArg::Int16(v.to_scalar().to_i16()?),
|
||||
ty::Int(IntTy::I32) => CArg::Int32(v.to_scalar().to_i32()?),
|
||||
ty::Int(IntTy::I64) => CArg::Int64(v.to_scalar().to_i64()?),
|
||||
ty::Int(IntTy::Isize) =>
|
||||
CArg::ISize(v.to_scalar().to_target_isize(cx)?.try_into().unwrap()),
|
||||
// the uints
|
||||
ty::Uint(UintTy::U8) => CArg::UInt8(v.to_scalar().to_u8()?),
|
||||
ty::Uint(UintTy::U16) => CArg::UInt16(v.to_scalar().to_u16()?),
|
||||
ty::Uint(UintTy::U32) => CArg::UInt32(v.to_scalar().to_u32()?),
|
||||
ty::Uint(UintTy::U64) => CArg::UInt64(v.to_scalar().to_u64()?),
|
||||
ty::Uint(UintTy::Usize) =>
|
||||
CArg::USize(v.to_scalar().to_target_usize(cx)?.try_into().unwrap()),
|
||||
_ => throw_unsup_format!("unsupported argument type for native call: {}", v.layout.ty),
|
||||
})
|
||||
}
|
@ -27,11 +27,12 @@ pub fn flagsplit(flags: &str) -> Vec<String> {
|
||||
flags.split(' ').map(str::trim).filter(|s| !s.is_empty()).map(str::to_string).collect()
|
||||
}
|
||||
|
||||
// Build the shared object file for testing external C function calls.
|
||||
fn build_so_for_c_ffi_tests() -> PathBuf {
|
||||
// Build the shared object file for testing native function calls.
|
||||
fn build_native_lib() -> PathBuf {
|
||||
let cc = option_env!("CC").unwrap_or("cc");
|
||||
// Target directory that we can write to.
|
||||
let so_target_dir = Path::new(&env::var_os("CARGO_TARGET_DIR").unwrap()).join("miri-extern-so");
|
||||
let so_target_dir =
|
||||
Path::new(&env::var_os("CARGO_TARGET_DIR").unwrap()).join("miri-native-lib");
|
||||
// Create the directory if it does not already exist.
|
||||
std::fs::create_dir_all(&so_target_dir)
|
||||
.expect("Failed to create directory for shared object file");
|
||||
@ -41,18 +42,18 @@ fn build_so_for_c_ffi_tests() -> PathBuf {
|
||||
"-shared",
|
||||
"-o",
|
||||
so_file_path.to_str().unwrap(),
|
||||
"tests/extern-so/test.c",
|
||||
"tests/native-lib/test.c",
|
||||
// Only add the functions specified in libcode.version to the shared object file.
|
||||
// This is to avoid automatically adding `malloc`, etc.
|
||||
// Source: https://anadoxin.org/blog/control-over-symbol-exports-in-gcc.html/
|
||||
"-fPIC",
|
||||
"-Wl,--version-script=tests/extern-so/libtest.map",
|
||||
"-Wl,--version-script=tests/native-lib/libtest.map",
|
||||
])
|
||||
.output()
|
||||
.expect("failed to generate shared object file for testing external C function calls");
|
||||
.expect("failed to generate shared object file for testing native function calls");
|
||||
if !cc_output.status.success() {
|
||||
panic!(
|
||||
"error in generating shared object file for testing external C function calls:\n{}",
|
||||
"error generating shared object file for testing native function calls:\n{}",
|
||||
String::from_utf8_lossy(&cc_output.stderr),
|
||||
);
|
||||
}
|
||||
@ -132,13 +133,12 @@ fn run_tests(
|
||||
config.program.args.push("--target".into());
|
||||
config.program.args.push(target.into());
|
||||
|
||||
// If we're testing the extern-so functionality, then build the shared object file for testing
|
||||
// If we're testing the native-lib functionality, then build the shared object file for testing
|
||||
// external C function calls and push the relevant compiler flag.
|
||||
if path.starts_with("tests/extern-so/") {
|
||||
assert!(cfg!(target_os = "linux"));
|
||||
let so_file_path = build_so_for_c_ffi_tests();
|
||||
let mut flag = std::ffi::OsString::from("-Zmiri-extern-so-file=");
|
||||
flag.push(so_file_path.into_os_string());
|
||||
if path.starts_with("tests/native-lib/") {
|
||||
let native_lib = build_native_lib();
|
||||
let mut flag = std::ffi::OsString::from("-Zmiri-native-lib=");
|
||||
flag.push(native_lib.into_os_string());
|
||||
config.program.args.push(flag);
|
||||
}
|
||||
|
||||
@ -292,10 +292,10 @@ fn main() -> Result<()> {
|
||||
tmpdir.path(),
|
||||
)?;
|
||||
if cfg!(target_os = "linux") {
|
||||
ui(Mode::Pass, "tests/extern-so/pass", &target, WithoutDependencies, tmpdir.path())?;
|
||||
ui(Mode::Pass, "tests/native-lib/pass", &target, WithoutDependencies, tmpdir.path())?;
|
||||
ui(
|
||||
Mode::Fail { require_patterns: true, rustfix: RustfixMode::Disabled },
|
||||
"tests/extern-so/fail",
|
||||
"tests/native-lib/fail",
|
||||
&target,
|
||||
WithoutDependencies,
|
||||
tmpdir.path(),
|
||||
|
Loading…
Reference in New Issue
Block a user