Hi, I'm trying to do something in a no_std environment with no allocator. I basically have this struct as an interface to some hardware, and underneath it calls some platform C APIs. The APIs expect me to provide a pointer to a C struct for initialization stuff, and once it has gone through all the initialization APIs, the address of the C struct must stay the same.
Because of this, I wasn't sure how to write a single new() that handles both returning the struct and properly initializing it with the platform APIs in one go. On return from new(), the value will be moved to the caller, and the underlying addresses of the C struct will change. Thus, I have one method for just returning an instance of the type in an unknown state, and another for setting it to a proper state:
Is there a better way of handling this so that it's just new() -> Result<Self, ()> and initialize() can go away? This used to be written in C++ and with the way C++ constructors worked (no copying out memory to the caller), it used to be that it was done in one go.
Or at least, if I can't consolidate the methods, is there a good way of forcing compile-time errors if a user calls other APIs before calling initialize()?
This is indeed not simple (to provide safely). The Rust standard library mechanism for types that need to not move is Pin, but as provided, it only helps with values that can be moved until their first pinned use. Possible solutions include:
When a global allocator is available, you write a wrapper type that boxes the value, and the public API on the wrapper doesn’t have any operations that would allow moving the value. This doesn’t apply to your case but it's the most common solution to this problem with FFI structs that must not move.
Live with the need to call initialize() separately; make it a method of Pin<&mut Self> and you get the immovability guarantee you need without needing to manage any memory explicitly (the caller decides how to obtain a Pin guarantee, and you make use of it).
I understand that the pin-init library builds on Pin to add the ability to initialize values in-place. I haven’t needed to use it myself, so I can't say how precisely fit it is for your purpose. There may be other libraries too.
There are active discussions on how to introduce something like pin-init into the language itself, at #t-lang/in-place-init on Zulip. However, I do not believe there is any functioning prototype yet.
Regardless of the exact solution to your problem, you should definitely expect the solution to involve Pin, because the whole point of Pin is decoupling “this value must not move” from how the place the value is stored in is allocated, which is needed when you can’t just Box::new() inside the implementation.
and you wouldn’t need a Self::new function, just MaybeUninit::new would likely suffice. Of course, it would increase the amount of unsafe needed; ideally, you find a good way to encapsulate it somehow. And Pin needs to be involved somewhere.
check out the moveit crate, which was implemented using Pin under the hood, but provides an "emplace constructor" API, sort of emulating C++ constructor semantics. with the help of it, you don't need to deal with MaybeUninit yourself.
example usage:
struct HardwareDriver {
device: HardwareThing,
_pin: PhantomPinned,
}
impl HardwareDriver {
// Defer construction until the final location is known.
fn new(args...) -> impl New<Output = Self> {
// `new::of()` **MOVES** the arguments to the destination location
// `new::by()` is the "lazy" version of `new::of()`
// `new::by_raw()` is the low level primitive.
new::by_raw(move |this: Pin<&mut MaybeUninit<Self>>| {
todo!("call C APIs to initialize")
})
}
}
// use of the `impl New` type must be enclosed in the `moveit!{}` macro
fn main() {
moveit! {
let driver = HardwareDriver::new(args...);
}
// note, the variable `driver` is a pinned pointer proxy type,
// so it can be moved like normal, but the pointee struct itself
// is `!Unpin`, which cannot be moved, because of `PhantomPinned`
}
Without allocator, you can't allocate a bunch of memory in new() to return it up, so you'll have to pass memory down from the caller. There, you just pin it to stack.
You could even pin Result<HardwareDriver, ()> with initial value of Err(()) or a suitable error type, fill it with new(this: Pin<&mut Result<Self, ()>>) and unwrap() as needed. That has some overhead on all the unwrappings though.
Since it seems to me that you are working with an embedded system and dealing with some kind of hardware drivers, I assume that the number of instances of a given structure is limited, perhaps even just one.
One solution could be to use a static global variable in your module to reserve the necessary memory and, in addition, use a atomic flag, counter, or something similar to keep track of which of your static memory blocks have been used.