-
Notifications
You must be signed in to change notification settings - Fork 45
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
In-place update #214
base: main
Are you sure you want to change the base?
In-place update #214
Conversation
Review changes with SemanticDiff. Analyzed 8 of 8 files. Overall, the semantic diff is 5% smaller than the GitHub diff.
|
@y86-dev Sorry for pinging you here and sorry to ask - but - would you be willing to review the implementation of the You don't need to review anything else. The whole other machinery introduced with this PR lays on the assumption, that what To translate a bit in English what Also - and if I understand your in-place initialization correctly - you don't have any such problem with the in-place initialization code in It is another topic that you probably don't care that much about unwinding panics in Same as we do for our main use case, which is running Yet, I would like to make sure, that |
No worries for pinging me. I will take a look, but I am rather busy at the moment. I might be able to get to it in the next week or so. |
Thank you. Next week is fine. This PR is not gate-keeping any other development ATM. |
(Just to mention that I'll later probably switch the "abort-on-unwinding-panic" code to use this clever trick, but that should be an insignificant change w.r.t. the logic of |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I left some comments about the implementation, overall it looks sensible.
In Rust for Linux, we have the need for InPlaceWrite<T>
I haven't yet added it to the github repo, but at some point I might get to it. You can use that trait to initialize a smart pointer that contains MaybeUninit<T>
.
Also - and if I understand your in-place initialization correctly - you don't have any such problem with the in-place initialization code in pinned-init, because - in the presence of unwinding panics - the worst that can happen is memory leaks, as the compiler will not drop your partially-initialized value. Which is OK.
Yes that is correct, as all of the containers that accept an initializer, contain only uninitialized memory, dropping them when a panic occurs is fine.
It is another topic that you probably don't care that much about unwinding panics in pinned-init, as this code works in the kernel, where - I suppose? - rustc operates with panic_abort.
It does operate with panic_abort
, but I still put some thought into making pinned-init
work with panics (ie nothing blows up if an initializer panics and gets unwinded).
I took a quick look at the use case of Apply::apply
and what I find a bit odd, is how the FromTLV::update_from_tlv
function is implemented. The whole "check if it is valid and then convert the fallible initializer to an infallible one" seems very strange. Maybe the correct return type of init_from_tlv
should actually be Result<impl Init<Self>>
?
sidenote: the FromTLV::init_from_tlv
function is probably not working as expected:
fn init_from_tlv(element: TLVElement<'a>) -> impl init::Init<Self, Error> {
unsafe {
init::init_from_closure(move |slot| {
core::ptr::write(slot, Self::from_tlv(&element)?);
Ok(())
})
}
}
The in-place initializer will overflow the stack, if the size of Self
is too big. That's because the call to Self::from_tlv
puts a temporary value on the stack.
Also, this function could take element
by reference instead of by value, then there would be no need to clone it in update_from_tlv
.
|
||
unsafe { | ||
// We can drop in place because we are sure that the following update | ||
// will not fail and also it should NOT panic |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might still panic though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed. I'll fix the comment.
{ | ||
// In the presence of `panic_unwind`: | ||
// | ||
// Catch the panic and abort immediately, because otherwise the program will continue | ||
// to run in an inconsistent state due to the potential double-drop of the value on | ||
// panic-unwind (we already called `core::ptr::drop_in_place` but the compiler does not know that!) | ||
|
||
extern crate std; | ||
|
||
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(update)); | ||
|
||
if result.is_err() { | ||
log::error!( | ||
"Panic detected during an infallible in-place update. Aborting the program." | ||
); | ||
std::process::abort(); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The way this is usually done is using a panic bomb:
struct Bomb;
impl Drop for Bomb {
fn drop(&mut self) {
log::error!("Panic detected during an infallible in-place update. Aborting the program.");
std::process::abort();
}
}
let bomb = Bomb;
/* do the possibly panicking stuff here */
std::mem::forget(bomb);
I don't know if doing it using catch_unwind
is better or has some kind of drawbacks, it's just that I haven't seen it in the wild.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A panic bomb is better for sure. It is just that I noticed it only after I submitted this PR. That's what I meant by this comment of mine in the PR:
(Just to mention that I'll later probably switch the "abort-on-unwinding-panic" code to use this clever trick, but that should be an insignificant change w.r.t. the logic of
ApplyInit::apply
.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah I missed that comment, all the better then :)
Thank you.
This means I could retire my own One danger with in-place initializing
Regardless which signature is used, the "check if it is valid before trying to init" is unavoidable. What is also unavoidable, is that if the initialization (after the check is already done) fails mid-way (it should not if the check is idempotent but then you never know) we can only turn the error into a panic and then any unwinding panic into a hard abort. The above we have to do regardless of the signature, so I don't think your I actually did start with
Agree completely!
I would turn it the other way around: In any case, the point is moot because Thanks again for taking the time to review - let me look at the inline comments as well now... |
Co-authored-by: y86-dev <[email protected]>
You could create a smart pointer type that has its backing memory in the
I meant that you do the check twice, since first you check and return if it errors and then you do the check again, but panic if the result isn't
That depends on when you know that the initializer will fail. If you can do the check without having the resulting memory on hand, then you can use
That's what I thought. It might be easier to only have one initialize/new function though (note that any
Ah yeah, then you have to take it by value.
Oh! Yeah probably makes sense to only use it by-value then and also implement copy :)
Glad I could help :) |
I'll address only ^^^ as it is the most important comment of all. I don't think a single check in Suppose I remove the call to What would then happen when I call And I think the answer is no. So where I'm going with that is that if we get a failure during the initialization, that should absolutely be an immediate abort, or else - even if your initializer is cleaning up the half-initialized memory behind itself - the rest of the code does not know that we have called |
This PR introduces an efficient
FromTLV::update_from_tlv
method with a default implementation that:FromTLV::init_from_tlv
into an infallible initialiserFromTLV::check_tlv
(which also has a default implementation) before attempting the update, so as to make sure, that the initializer can indeed be safely turned into an infallible one (i.e. it will not fail in the middle due to a malformed TLV stream)Note that
FromTLV::update_from_tlv
assumes thatFromTLV::check_tlv
would be idempotent - i.e. calling it multiple times on the sameTLVElement
would yield identical results. However, that should be indeed the case anyway.The benefit of having
FromTLV::update_from_tlv
is that it gives us cheap, transactional in-place updates, where either a value is in-place updated with the initializer completely, or the value is not updated at all. Basically, an "in-place" equivalent of:The problem with the above
update
method, and the reason why we needFromTLV::update_from_tlv
is - as usual - stack memory; i.e. it is transactional w.r.t. updating the value, but not cheap. The problem is, thenew
value is first materialized on-stack, and only after that it is moved to its final location (*self
) - a behavior, which rustc might or might not optimize.While we now have
FromTLV::init_from_tlv
which avoids the on-stack materialization ofFromTLV::from_tlv
, we don't have any "update" equivalent toFromTLV::init_from_tlv
, which this PR introduces.NOTE: This PR is deliberately in Draft and will stay in such, until I can discuss with @y86-dev (the author of the
pinned-init
crate and a RfL contributor) that thecrate::utils::init::ApplyInit
extension trait introduced with this PR is sound.