From 7fde02ea53f423ca25740e9339d8ff1db8f73052 Mon Sep 17 00:00:00 2001 From: Strophox Date: Wed, 31 Jul 2024 09:41:14 +0200 Subject: enable Miri to pass const pointers through FFI Co-authored-by: Ralf Jung --- src/tools/miri/src/alloc_addresses/mod.rs | 68 +++++++++++++++++- src/tools/miri/src/concurrency/thread.rs | 2 +- src/tools/miri/src/lib.rs | 3 + src/tools/miri/src/machine.rs | 25 +++++++ src/tools/miri/src/shims/native_lib.rs | 13 ++++ src/tools/miri/tests/native-lib/libtest.map | 12 ---- src/tools/miri/tests/native-lib/native-lib.map | 20 ++++++ .../miri/tests/native-lib/pass/call_extern_c_fn.rs | 46 ------------ .../tests/native-lib/pass/call_extern_c_fn.stdout | 1 - .../miri/tests/native-lib/pass/ptr_read_access.rs | 82 ++++++++++++++++++++++ .../tests/native-lib/pass/ptr_read_access.stdout | 1 + .../miri/tests/native-lib/pass/scalar_arguments.rs | 46 ++++++++++++ .../tests/native-lib/pass/scalar_arguments.stdout | 1 + src/tools/miri/tests/native-lib/ptr_read_access.c | 47 +++++++++++++ src/tools/miri/tests/native-lib/scalar_arguments.c | 27 +++++++ src/tools/miri/tests/native-lib/test.c | 27 ------- src/tools/miri/tests/ui.rs | 13 +++- 17 files changed, 343 insertions(+), 91 deletions(-) delete mode 100644 src/tools/miri/tests/native-lib/libtest.map create mode 100644 src/tools/miri/tests/native-lib/native-lib.map delete mode 100644 src/tools/miri/tests/native-lib/pass/call_extern_c_fn.rs delete mode 100644 src/tools/miri/tests/native-lib/pass/call_extern_c_fn.stdout create mode 100644 src/tools/miri/tests/native-lib/pass/ptr_read_access.rs create mode 100644 src/tools/miri/tests/native-lib/pass/ptr_read_access.stdout create mode 100644 src/tools/miri/tests/native-lib/pass/scalar_arguments.rs create mode 100644 src/tools/miri/tests/native-lib/pass/scalar_arguments.stdout create mode 100644 src/tools/miri/tests/native-lib/ptr_read_access.c create mode 100644 src/tools/miri/tests/native-lib/scalar_arguments.c delete mode 100644 src/tools/miri/tests/native-lib/test.c (limited to 'src/tools/miri') diff --git a/src/tools/miri/src/alloc_addresses/mod.rs b/src/tools/miri/src/alloc_addresses/mod.rs index ed955e78c3e..76c68add8cd 100644 --- a/src/tools/miri/src/alloc_addresses/mod.rs +++ b/src/tools/miri/src/alloc_addresses/mod.rs @@ -42,6 +42,11 @@ pub struct GlobalStateInner { /// they do not have an `AllocExtra`. /// This is the inverse of `int_to_ptr_map`. base_addr: FxHashMap, + /// Temporarily store prepared memory space for global allocations the first time their memory + /// address is required. This is used to ensure that the memory is allocated before Miri assigns + /// it an internal address, which is important for matching the internal address to the machine + /// address so FFI can read from pointers. + prepared_alloc_bytes: FxHashMap, /// A pool of addresses we can reuse for future allocations. reuse: ReusePool, /// Whether an allocation has been exposed or not. This cannot be put @@ -59,6 +64,7 @@ impl VisitProvenance for GlobalStateInner { let GlobalStateInner { int_to_ptr_map: _, base_addr: _, + prepared_alloc_bytes: _, reuse: _, exposed: _, next_base_addr: _, @@ -78,6 +84,7 @@ impl GlobalStateInner { GlobalStateInner { int_to_ptr_map: Vec::default(), base_addr: FxHashMap::default(), + prepared_alloc_bytes: FxHashMap::default(), reuse: ReusePool::new(config), exposed: FxHashSet::default(), next_base_addr: stack_addr, @@ -166,7 +173,39 @@ trait EvalContextExtPriv<'tcx>: crate::MiriInterpCxExt<'tcx> { assert!(!matches!(kind, AllocKind::Dead)); // This allocation does not have a base address yet, pick or reuse one. - let base_addr = if let Some((reuse_addr, clock)) = global_state.reuse.take_addr( + let base_addr = if ecx.machine.native_lib.is_some() { + // In native lib mode, we use the "real" address of the bytes for this allocation. + // This ensures the interpreted program and native code have the same view of memory. + match kind { + AllocKind::LiveData => { + let ptr = if ecx.tcx.try_get_global_alloc(alloc_id).is_some() { + // For new global allocations, we always pre-allocate the memory to be able use the machine address directly. + let prepared_bytes = MiriAllocBytes::zeroed(size, align) + .unwrap_or_else(|| { + panic!("Miri ran out of memory: cannot create allocation of {size:?} bytes") + }); + let ptr = prepared_bytes.as_ptr(); + // Store prepared allocation space to be picked up for use later. + global_state.prepared_alloc_bytes.try_insert(alloc_id, prepared_bytes).unwrap(); + ptr + } else { + ecx.get_alloc_bytes_unchecked_raw(alloc_id)? + }; + // Ensure this pointer's provenance is exposed, so that it can be used by FFI code. + ptr.expose_provenance().try_into().unwrap() + } + AllocKind::Function | AllocKind::VTable => { + // Allocate some dummy memory to get a unique address for this function/vtable. + let alloc_bytes = MiriAllocBytes::from_bytes(&[0u8; 1], Align::from_bytes(1).unwrap()); + // We don't need to expose these bytes as nobody is allowed to access them. + let addr = alloc_bytes.as_ptr().addr().try_into().unwrap(); + // Leak the underlying memory to ensure it remains unique. + std::mem::forget(alloc_bytes); + addr + } + AllocKind::Dead => unreachable!() + } + } else if let Some((reuse_addr, clock)) = global_state.reuse.take_addr( &mut *rng, size, align, @@ -318,6 +357,33 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { Ok(base_ptr.wrapping_offset(offset, ecx)) } + // This returns some prepared `MiriAllocBytes`, either because `addr_from_alloc_id` reserved + // memory space in the past, or by doing the pre-allocation right upon being called. + fn get_global_alloc_bytes(&self, id: AllocId, kind: MemoryKind, bytes: &[u8], align: Align) -> InterpResult<'tcx, MiriAllocBytes> { + let ecx = self.eval_context_ref(); + Ok(if ecx.machine.native_lib.is_some() { + // In native lib mode, MiriAllocBytes for global allocations are handled via `prepared_alloc_bytes`. + // This additional call ensures that some `MiriAllocBytes` are always prepared. + ecx.addr_from_alloc_id(id, kind)?; + let mut global_state = ecx.machine.alloc_addresses.borrow_mut(); + // The memory we need here will have already been allocated during an earlier call to + // `addr_from_alloc_id` for this allocation. So don't create a new `MiriAllocBytes` here, instead + // fetch the previously prepared bytes from `prepared_alloc_bytes`. + let mut prepared_alloc_bytes = global_state + .prepared_alloc_bytes + .remove(&id) + .unwrap_or_else(|| panic!("alloc bytes for {id:?} have not been prepared")); + // Sanity-check that the prepared allocation has the right size and alignment. + assert!(prepared_alloc_bytes.as_ptr().is_aligned_to(align.bytes_usize())); + assert_eq!(prepared_alloc_bytes.len(), bytes.len()); + // Copy allocation contents into prepared memory. + prepared_alloc_bytes.copy_from_slice(bytes); + prepared_alloc_bytes + } else { + MiriAllocBytes::from_bytes(std::borrow::Cow::Borrowed(&*bytes), align) + }) + } + /// When a pointer is used for a memory access, this computes where in which allocation the /// access is going. fn ptr_get_alloc( diff --git a/src/tools/miri/src/concurrency/thread.rs b/src/tools/miri/src/concurrency/thread.rs index a4d3e3f7af3..37d4a2663e0 100644 --- a/src/tools/miri/src/concurrency/thread.rs +++ b/src/tools/miri/src/concurrency/thread.rs @@ -887,7 +887,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { let alloc = this.ctfe_query(|tcx| tcx.eval_static_initializer(def_id))?; // We make a full copy of this allocation. let mut alloc = - alloc.inner().adjust_from_tcx(&this.tcx, |ptr| this.global_root_pointer(ptr))?; + alloc.inner().adjust_from_tcx(&this.tcx, |bytes, align| Ok(MiriAllocBytes::from_bytes(std::borrow::Cow::Borrowed(bytes), align)), |ptr| this.global_root_pointer(ptr))?; // This allocation will be deallocated when the thread dies, so it is not in read-only memory. alloc.mutability = Mutability::Mut; // Create a fresh allocation with this content. diff --git a/src/tools/miri/src/lib.rs b/src/tools/miri/src/lib.rs index f796e845a23..f70ca098fb9 100644 --- a/src/tools/miri/src/lib.rs +++ b/src/tools/miri/src/lib.rs @@ -12,6 +12,9 @@ #![feature(let_chains)] #![feature(trait_upcasting)] #![feature(strict_overflow_ops)] +#![feature(strict_provenance)] +#![feature(exposed_provenance)] +#![feature(pointer_is_aligned_to)] // Configure clippy and other lints #![allow( clippy::collapsible_else_if, diff --git a/src/tools/miri/src/machine.rs b/src/tools/miri/src/machine.rs index a7493d48d6a..35bef5e6ed5 100644 --- a/src/tools/miri/src/machine.rs +++ b/src/tools/miri/src/machine.rs @@ -1,6 +1,7 @@ //! Global machine state as well as implementation of the interpreter engine //! `Machine` trait. +use std::borrow::Cow; use std::cell::RefCell; use std::collections::hash_map::Entry; use std::fmt; @@ -1225,6 +1226,30 @@ impl<'tcx> Machine<'tcx> for MiriMachine<'tcx> { }) } + /// Called to adjust global allocations to the Provenance and AllocExtra of this machine. + /// + /// If `alloc` contains pointers, then they are all pointing to globals. + /// + /// This should avoid copying if no work has to be done! If this returns an owned + /// allocation (because a copy had to be done to adjust things), machine memory will + /// cache the result. (This relies on `AllocMap::get_or` being able to add the + /// owned allocation to the map even when the map is shared.) + fn adjust_global_allocation<'b>( + ecx: &InterpCx<'tcx, Self>, + id: AllocId, + alloc: &'b Allocation, + ) -> InterpResult<'tcx, Cow<'b, Allocation>> + { + let kind = Self::GLOBAL_KIND.unwrap().into(); + let alloc = alloc.adjust_from_tcx(&ecx.tcx, + |bytes, align| ecx.get_global_alloc_bytes(id, kind, bytes, align), + |ptr| ecx.global_root_pointer(ptr), + )?; + let extra = + Self::init_alloc_extra(ecx, id, kind, alloc.size(), alloc.align)?; + Ok(Cow::Owned(alloc.with_extra(extra))) + } + #[inline(always)] fn before_memory_read( _tcx: TyCtxtAt<'tcx>, diff --git a/src/tools/miri/src/shims/native_lib.rs b/src/tools/miri/src/shims/native_lib.rs index 40697e17ba1..25d96fbd733 100644 --- a/src/tools/miri/src/shims/native_lib.rs +++ b/src/tools/miri/src/shims/native_lib.rs @@ -194,6 +194,8 @@ enum CArg { UInt64(u64), /// usize. USize(usize), + /// Raw pointer, stored as C's `void*`. + RawPtr(*mut std::ffi::c_void), } impl<'a> CArg { @@ -210,6 +212,7 @@ impl<'a> CArg { CArg::UInt32(i) => ffi::arg(i), CArg::UInt64(i) => ffi::arg(i), CArg::USize(i) => ffi::arg(i), + CArg::RawPtr(i) => ffi::arg(i), } } } @@ -234,6 +237,16 @@ fn imm_to_carg<'tcx>(v: ImmTy<'tcx>, cx: &impl HasDataLayout) -> InterpResult<'t 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()), + ty::RawPtr(_, mutability) => { + // Arbitrary mutable pointer accesses are not currently supported in Miri. + if mutability.is_mut() { + throw_unsup_format!("unsupported mutable pointer type for native call: {}", v.layout.ty); + } else { + let s = v.to_scalar().to_pointer(cx)?.addr(); + // This relies on the `expose_provenance` in `addr_from_alloc_id`. + CArg::RawPtr(std::ptr::with_exposed_provenance_mut(s.bytes_usize())) + } + }, _ => throw_unsup_format!("unsupported argument type for native call: {}", v.layout.ty), }) } diff --git a/src/tools/miri/tests/native-lib/libtest.map b/src/tools/miri/tests/native-lib/libtest.map deleted file mode 100644 index a57a4dc149f..00000000000 --- a/src/tools/miri/tests/native-lib/libtest.map +++ /dev/null @@ -1,12 +0,0 @@ -CODEABI_1.0 { - # Define which symbols to export. - global: - add_one_int; - printer; - test_stack_spill; - get_unsigned_int; - add_int16; - add_short_to_long; - # The rest remains private. - local: *; -}; diff --git a/src/tools/miri/tests/native-lib/native-lib.map b/src/tools/miri/tests/native-lib/native-lib.map new file mode 100644 index 00000000000..7e3bd19622a --- /dev/null +++ b/src/tools/miri/tests/native-lib/native-lib.map @@ -0,0 +1,20 @@ +CODEABI_1.0 { + # Define which symbols to export. + global: + # scalar_arguments.c + add_one_int; + printer; + test_stack_spill; + get_unsigned_int; + add_int16; + add_short_to_long; + + # ptr_read_access.c + print_pointer; + access_simple; + access_nested; + access_static; + + # The rest remains private. + local: *; +}; diff --git a/src/tools/miri/tests/native-lib/pass/call_extern_c_fn.rs b/src/tools/miri/tests/native-lib/pass/call_extern_c_fn.rs deleted file mode 100644 index 1e1d0b11e99..00000000000 --- a/src/tools/miri/tests/native-lib/pass/call_extern_c_fn.rs +++ /dev/null @@ -1,46 +0,0 @@ -//@only-target-linux -//@only-on-host - -extern "C" { - fn add_one_int(x: i32) -> i32; - fn add_int16(x: i16) -> i16; - fn test_stack_spill( - a: i32, - b: i32, - c: i32, - d: i32, - e: i32, - f: i32, - g: i32, - h: i32, - i: i32, - j: i32, - k: i32, - l: i32, - ) -> i32; - fn add_short_to_long(x: i16, y: i64) -> i64; - fn get_unsigned_int() -> u32; - fn printer(); -} - -fn main() { - unsafe { - // test function that adds 2 to a provided int - assert_eq!(add_one_int(1), 3); - - // test function that takes the sum of its 12 arguments - assert_eq!(test_stack_spill(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12), 78); - - // test function that adds 3 to a 16 bit int - assert_eq!(add_int16(-1i16), 2i16); - - // test function that adds an i16 to an i64 - assert_eq!(add_short_to_long(-1i16, 123456789123i64), 123456789122i64); - - // test function that returns -10 as an unsigned int - assert_eq!(get_unsigned_int(), (-10i32) as u32); - - // test void function that prints from C - printer(); - } -} diff --git a/src/tools/miri/tests/native-lib/pass/call_extern_c_fn.stdout b/src/tools/miri/tests/native-lib/pass/call_extern_c_fn.stdout deleted file mode 100644 index 7ba13d2d7b0..00000000000 --- a/src/tools/miri/tests/native-lib/pass/call_extern_c_fn.stdout +++ /dev/null @@ -1 +0,0 @@ -printing from C diff --git a/src/tools/miri/tests/native-lib/pass/ptr_read_access.rs b/src/tools/miri/tests/native-lib/pass/ptr_read_access.rs new file mode 100644 index 00000000000..d8e6209839e --- /dev/null +++ b/src/tools/miri/tests/native-lib/pass/ptr_read_access.rs @@ -0,0 +1,82 @@ +//@only-target-linux +//@only-on-host + +fn main() { + test_pointer(); + + test_simple(); + + test_nested(); + + test_static(); +} + +// Test void function that dereferences a pointer and prints its contents from C. +fn test_pointer() { + extern "C" { + fn print_pointer(ptr: *const i32); + } + + let x = 42; + + unsafe { print_pointer(&x) }; +} + +// Test function that dereferences a simple struct pointer and accesses a field. +fn test_simple() { + #[repr(C)] + struct Simple { + field: i32 + } + + extern "C" { + fn access_simple(s_ptr: *const Simple) -> i32; + } + + let simple = Simple { field: -42 }; + + assert_eq!(unsafe { access_simple(&simple) }, -42); +} + +// Test function that dereferences nested struct pointers and accesses fields. +fn test_nested() { + use std::ptr::NonNull; + + #[derive(Debug, PartialEq, Eq)] + #[repr(C)] + struct Nested { + value: i32, + next: Option>, + } + + extern "C" { + fn access_nested(n_ptr: *const Nested) -> i32; + } + + let mut nested_0 = Nested { value: 97, next: None }; + let mut nested_1 = Nested { value: 98, next: NonNull::new(&mut nested_0) }; + let nested_2 = Nested { value: 99, next: NonNull::new(&mut nested_1) }; + + assert_eq!(unsafe { access_nested(&nested_2) }, 97); +} + +// Test function that dereferences static struct pointers and accesses fields. +fn test_static() { + + #[repr(C)] + struct Static { + value: i32, + recurse: &'static Static, + } + + extern "C" { + fn access_static(n_ptr: *const Static) -> i32; + } + + static STATIC: Static = Static { + value: 9001, + recurse: &STATIC, + }; + + assert_eq!(unsafe { access_static(&STATIC) }, 9001); +} diff --git a/src/tools/miri/tests/native-lib/pass/ptr_read_access.stdout b/src/tools/miri/tests/native-lib/pass/ptr_read_access.stdout new file mode 100644 index 00000000000..1a8799abfc9 --- /dev/null +++ b/src/tools/miri/tests/native-lib/pass/ptr_read_access.stdout @@ -0,0 +1 @@ +printing pointer dereference from C: 42 diff --git a/src/tools/miri/tests/native-lib/pass/scalar_arguments.rs b/src/tools/miri/tests/native-lib/pass/scalar_arguments.rs new file mode 100644 index 00000000000..1e1d0b11e99 --- /dev/null +++ b/src/tools/miri/tests/native-lib/pass/scalar_arguments.rs @@ -0,0 +1,46 @@ +//@only-target-linux +//@only-on-host + +extern "C" { + fn add_one_int(x: i32) -> i32; + fn add_int16(x: i16) -> i16; + fn test_stack_spill( + a: i32, + b: i32, + c: i32, + d: i32, + e: i32, + f: i32, + g: i32, + h: i32, + i: i32, + j: i32, + k: i32, + l: i32, + ) -> i32; + fn add_short_to_long(x: i16, y: i64) -> i64; + fn get_unsigned_int() -> u32; + fn printer(); +} + +fn main() { + unsafe { + // test function that adds 2 to a provided int + assert_eq!(add_one_int(1), 3); + + // test function that takes the sum of its 12 arguments + assert_eq!(test_stack_spill(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12), 78); + + // test function that adds 3 to a 16 bit int + assert_eq!(add_int16(-1i16), 2i16); + + // test function that adds an i16 to an i64 + assert_eq!(add_short_to_long(-1i16, 123456789123i64), 123456789122i64); + + // test function that returns -10 as an unsigned int + assert_eq!(get_unsigned_int(), (-10i32) as u32); + + // test void function that prints from C + printer(); + } +} diff --git a/src/tools/miri/tests/native-lib/pass/scalar_arguments.stdout b/src/tools/miri/tests/native-lib/pass/scalar_arguments.stdout new file mode 100644 index 00000000000..7ba13d2d7b0 --- /dev/null +++ b/src/tools/miri/tests/native-lib/pass/scalar_arguments.stdout @@ -0,0 +1 @@ +printing from C diff --git a/src/tools/miri/tests/native-lib/ptr_read_access.c b/src/tools/miri/tests/native-lib/ptr_read_access.c new file mode 100644 index 00000000000..03b9189e2e8 --- /dev/null +++ b/src/tools/miri/tests/native-lib/ptr_read_access.c @@ -0,0 +1,47 @@ +#include + +/* Test: test_pointer */ + +void print_pointer(const int *ptr) { + printf("printing pointer dereference from C: %d\n", *ptr); +} + +/* Test: test_simple */ + +typedef struct Simple { + int field; +} Simple; + +int access_simple(const Simple *s_ptr) { + return s_ptr->field; +} + +/* Test: test_nested */ + +typedef struct Nested { + int value; + struct Nested *next; +} Nested; + +// Returns the innermost/last value of a Nested pointer chain. +int access_nested(const Nested *n_ptr) { + // Edge case: `n_ptr == NULL` (i.e. first Nested is None). + if (!n_ptr) { return 0; } + + while (n_ptr->next) { + n_ptr = n_ptr->next; + } + + return n_ptr->value; +} + +/* Test: test_static */ + +typedef struct Static { + int value; + struct Static *recurse; +} Static; + +int access_static(const Static *s_ptr) { + return s_ptr->recurse->recurse->value; +} diff --git a/src/tools/miri/tests/native-lib/scalar_arguments.c b/src/tools/miri/tests/native-lib/scalar_arguments.c new file mode 100644 index 00000000000..68714f1743b --- /dev/null +++ b/src/tools/miri/tests/native-lib/scalar_arguments.c @@ -0,0 +1,27 @@ +#include + +int add_one_int(int x) { + return 2 + x; +} + +void printer() { + printf("printing from C\n"); +} + +// function with many arguments, to test functionality when some args are stored +// on the stack +int test_stack_spill(int a, int b, int c, int d, int e, int f, int g, int h, int i, int j, int k, int l) { + return a+b+c+d+e+f+g+h+i+j+k+l; +} + +unsigned int get_unsigned_int() { + return -10; +} + +short add_int16(short x) { + return x + 3; +} + +long add_short_to_long(short x, long y) { + return x + y; +} diff --git a/src/tools/miri/tests/native-lib/test.c b/src/tools/miri/tests/native-lib/test.c deleted file mode 100644 index 68714f1743b..00000000000 --- a/src/tools/miri/tests/native-lib/test.c +++ /dev/null @@ -1,27 +0,0 @@ -#include - -int add_one_int(int x) { - return 2 + x; -} - -void printer() { - printf("printing from C\n"); -} - -// function with many arguments, to test functionality when some args are stored -// on the stack -int test_stack_spill(int a, int b, int c, int d, int e, int f, int g, int h, int i, int j, int k, int l) { - return a+b+c+d+e+f+g+h+i+j+k+l; -} - -unsigned int get_unsigned_int() { - return -10; -} - -short add_int16(short x) { - return x + 3; -} - -long add_short_to_long(short x, long y) { - return x + y; -} diff --git a/src/tools/miri/tests/ui.rs b/src/tools/miri/tests/ui.rs index 9cbcf6e42a7..c510ef95c30 100644 --- a/src/tools/miri/tests/ui.rs +++ b/src/tools/miri/tests/ui.rs @@ -36,18 +36,25 @@ fn build_native_lib() -> PathBuf { // 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"); - let so_file_path = so_target_dir.join("libtestlib.so"); + let so_file_path = so_target_dir.join("native-lib.so"); let cc_output = Command::new(cc) .args([ "-shared", "-o", so_file_path.to_str().unwrap(), - "tests/native-lib/test.c", + // FIXME: Automate gathering of all relevant C source files in the directory. + "tests/native-lib/scalar_arguments.c", + "tests/native-lib/ptr_read_access.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/native-lib/libtest.map", + "-Wl,--version-script=tests/native-lib/native-lib.map", + // Ensure we notice serious problems in the C code. + "-Wall", + "-Wextra", + "-Wpedantic", + "-Werror", ]) .output() .expect("failed to generate shared object file for testing native function calls"); -- cgit 1.4.1-3-g733a5