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:
bors 2024-05-10 16:33:14 +00:00
commit 6f4c7d997e
16 changed files with 283 additions and 333 deletions

View File

@ -9,5 +9,5 @@ tex/*/out
perf.data
perf.data.old
flamegraph.svg
tests/extern-so/libtestlib.so
tests/native-lib/libtestlib.so
.auto-*

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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: _,

View File

@ -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),
}
}
}

View File

@ -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);
}
}

View File

@ -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;

View 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),
})
}

View File

@ -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(),