native-lib: allow passing structs as arguments#4466
native-lib: allow passing structs as arguments#4466RalfJung merged 2 commits intorust-lang:masterfrom
Conversation
ca18991 to
4e6a464
Compare
|
|
||
| fn main() { | ||
| let pass_me = PassMe { value: 42, other_value: 1337 }; | ||
| unsafe { pass_struct(pass_me) }; //~ ERROR: Undefined Behavior: passing a non-#[repr(C)] struct over FFI |
There was a problem hiding this comment.
Is this correct? I think it's UB, but maybe if the user intentionally sets #[allow(improper_ctypes)] we might just want to report this as unsupported instead of UB?
There was a problem hiding this comment.
Yeah, reporting this as "unsupported" is better.
1faf540 to
e9670f0
Compare
|
Think this is ready for review. I split off a bunch of the new code this introduces into a different file to not pollute @rustbot ready |
7cd63db to
532a735
Compare
|
Seems like there's some trouble here, nvm. @rustbot author |
|
Reminder, once the PR becomes ready for a review, use |
809f60e to
edfa595
Compare
|
Should be fixed. Also bumped the libffi version since apparently there was a soundness issue that got resolved since the previous version we were on; that wasn't the cause of my test failures, but it's probably smart to include it anyways. There's a couple @rustbot ready |
685a17c to
a5c0d97
Compare
| unsafe { cif.call(fun, &arg_ptrs) } | ||
| } | ||
|
|
||
| /// A wrapper type for `libffi::middle::Type` which also holds a pointer to the data. |
There was a problem hiding this comment.
Can you explain what this means in a self-contained way, rather than referencing other concepts the reader is unlikely to know?
Also, it's odd to lead with this being a "type" when it is called "arg".
| } | ||
| } | ||
|
|
||
| /// An owning form of `FfiArg`. |
There was a problem hiding this comment.
The prefixes "C" and "Ffi" don't do a great job reflecting "owned" and borrowed.
So what about OwnedArg and BorrowedArg? The "downcast" function (also an odd name, what does it downcast?) can then be called borrow.
CPrimitive could be called ScalarArg as that seems to match what we use it for.
Also, please generally define types bottom-up and with increasing complexity/subtlety, to simplify reading the file in order:
- ScalarArg
- OwnedArg
- BorrowedArg
| /// Struct with its computed type layout and bytes. | ||
| Struct(FfiType, Box<[u8]>), |
There was a problem hiding this comment.
This doesn't have to be a struct, right? It can be any type that FfiType can represent, which includes basic integers.
There was a problem hiding this comment.
I'll change the name, presumably the next PR idea which you brought up on Zulip would make this enum redundant anyway
|
|
||
| fn main() { | ||
| let pass_me = PassMe { value: 42, other_value: 1337 }; | ||
| unsafe { pass_struct(pass_me) }; //~ ERROR: Undefined Behavior: passing a non-#[repr(C)] struct over FFI |
There was a problem hiding this comment.
Yeah, reporting this as "unsupported" is better.
| /// Test passing a basic struct as an argument. | ||
| fn test_pass_struct() { |
There was a problem hiding this comment.
The file these tests are in is called "scalar_arguments". Do these arguments you are passing here ,ook like scalars to you? :)
Please make a new file for aggregate arguments.
| args: &'tcx ty::List<ty::GenericArg<'tcx>>, | ||
| ) -> InterpResult<'tcx, FfiType> { | ||
| // TODO: is this correct? Maybe `repr(transparent)` when the inner field | ||
| // is itself `repr(c)` is ok? |
There was a problem hiding this comment.
Yes, that would be okay. But let's leave it to a future PR, since a repr(transparent) wrapper around e.g. i32 should also be supported and that'll be a bit more interesting -- it's going to be an Immediate.
| if let Some(prov) = ptr.provenance { | ||
| // The first time this happens, print a warning. | ||
| if !this.machine.native_call_mem_warned.replace(true) { | ||
| // Newly set, so first time we get here. | ||
| this.emit_diagnostic(NonHaltingDiagnostic::NativeCallSharedMem { tracing }); | ||
| } | ||
|
|
||
| this.expose_provenance(prov)?; | ||
| }; |
There was a problem hiding this comment.
The same snippet will be needed in the struct case, so please make this a helper function.
|
Hm, I rebased on master to make use of |
|
This PR was rebased onto a different master commit. Here's a range-diff highlighting what actually changed. Rebasing is a normal part of keeping PRs up to date, so no action is needed—this note is just to help reviewers. |
Turns out |
| .alloc_id_from_addr( | ||
| mplace_ptr.addr().bytes(), |
There was a problem hiding this comment.
Here you are doing the equivalent of casting a pointer to an integer and back. Please don't. You can get the alloc ID from the pointer directly.
There was a problem hiding this comment.
Specifically, please use ptr_get_alloc_id. That also gives you the offset inside the allocation.
However, you'll need to first separately handle the zero-sized case. mplace.ptr() might not point to an Allocation if the type has size 0.
There was a problem hiding this comment.
Are ZSTs allowed over FFI? I was under the impression that there's no way to represent them in C so I figured just erroring here makes sense
There was a problem hiding this comment.
I guess erroring is fine but that still require a separate check to print a non-obscure error.
| alloc_ptr.with_addr(mplace_ptr.addr().bytes_usize()), | ||
| mplace.layout.size.bytes_usize(), | ||
| ) | ||
| .to_vec() |
There was a problem hiding this comment.
Round-tripping through a Vec seems odd?
There was a problem hiding this comment.
Fair, I guess it's pretty easily avoidable
|
Should be good. I couldn't use your suggestion of I had also added some tests for that code (passing structs that were |
You should be calling |
| let mplace_ptr = mplace.ptr(); | ||
| let sz = mplace.layout.size.bytes_usize(); | ||
| if sz == 0 { | ||
| throw_unsup_format!("Attempting to pass a ZST over FFI"); |
There was a problem hiding this comment.
| throw_unsup_format!("Attempting to pass a ZST over FFI"); | |
| throw_unsup_format!("attempting to pass a ZST over FFI"); |
| let sz = imm.layout.size.bytes_usize(); | ||
| // TODO: Is this actually ok? Seems like the only way to figure | ||
| // out how the scalars are laid out relative to each other. | ||
| let align_second = match imm.layout.backend_repr { | ||
| rustc_abi::BackendRepr::Scalar(_) => 1, | ||
| rustc_abi::BackendRepr::ScalarPair(_, sc2) => sc2.align(this).bytes_usize(), | ||
| _ => unreachable!(), | ||
| }; | ||
| // How many bytes to skip between scalars if necessary for alignment. | ||
| let skip = sz_first.next_multiple_of(align_second).strict_sub(sz_first); | ||
|
|
There was a problem hiding this comment.
Have a look at https://github.com/rust-lang/rust/blob/ece1397e3ff40e7f8a411981844a8214659da970/compiler/rustc_const_eval/src/interpret/place.rs#L716-L732 for how to determine the offset of the 2nd field.
| match this.data_layout().endian { | ||
| rustc_abi::Endian::Little => { |
There was a problem hiding this comment.
You should not be doing your own endianess handling. write_target_uint can do that for you.
|
@rustbot author |
|
@rustbot ready |
| // This relies on the `expose_provenance` in the `visit_reachable_allocs` callback | ||
| // below to expose the actual interpreter-level allocation. |
There was a problem hiding this comment.
This comment is entirely in the wrong place now... previously it referred to a std::ptr::with_exposed_provenance_mut, now it stopped making any sense. Please ask before just taking random guesses as to where to put such comments.
There was a problem hiding this comment.
Is it wrong? If either scalar is a pointer, the code under the comment exposes provenance only in the AM; then later when visit_reachable_allocs is called it'll do alloc.get_bytes_unchecked_raw().expose_provenance() on the allocation said pointer points to. Unless I misunderstood the meaning of that comment, that's what it referred to in the first place
There was a problem hiding this comment.
It's correct that the code below only exposes the ptr in the AM. But the claim that this act somehow relies on the ptr also being exposed in the "host" makes no sense. That referred to the with_exposed_provenance_mut, which as per its documentation requires a prior call to expose_provenance, and the comment was explaining where that expose_provenance happened.
The new code side-steps the with_exposed_provenance_mut, making it less clear where to put the comment. But this here is definitely the wrong spot.
d3d20cc to
0b04a2d
Compare
|
I've done some refactoring and cleanup, found it easier to just edit the code than write it out.^^ Please take a look at the commit I added and use that to train your model for future Miri PRs. ;) |
| // bytes are part of this allocation and initialised. They might be marked | ||
| // as uninit in Miri, but all bytes returned by `MiriAllocBytes` are | ||
| // initialised. | ||
| unsafe { |
There was a problem hiding this comment.
In particular, I got rid of some unsafe code here.
0b04a2d to
eff2b42
Compare
|
Ty! Glad this is good now ^^ |
Blocked on #4456 currently to make rebasing easier when that lands.This allows passing arbitrary structs as arguments to native code; receiving a struct as a return value is not yet implemented. Also includes a couple of tests.I figured the review queue looked a bit too empty :D